#!/bin/ksh
#
# $OpenBSD: portcheck,v 1.150 2025/10/24 14:25:34 rsadowski 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
	# idea from DPB/Vars.pm
	test_mf=$(cat <<EOF
DUMMY_PACKAGE = yes
.include <bsd.port.mk>
EOF
	)
	IFS=:
	set -A portsdir_path -- \
		$( (echo "$test_mf" | env FLAVOR= make -C / -f - 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 and/or
# when SKIPDIR framework is used
set -A make_args -- \
	SKIPDIR= STARTAFTER= STARTDIR= \
	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

############################################################
# Support for SKIPDIR, STARTDIR and STARTAFTER, see ports(7)
#

SKIPDIR=${SKIPDIR:-}
STARTDIR=${STARTDIR:-}
STARTAFTER=${STARTAFTER:-}
if [[ -n $STARTAFTER ]]; then
	STARTDIR=$STARTAFTER
	SKIPDIR="$SKIPDIR $STARTAFTER"
fi

path_parts_count() {
	(IFS=/; set -- $1; echo $#)
}

# true if directory given should be skipped based on STARTDIR
# and/or SKIPDIR variable
skip_dir() {
	$rootrun || return 1
	local dir=$(readlink -f "$1")
	dir=${dir##$portsdir*(/)}
	local startpartscount=$(path_parts_count "$STARTDIR")
	local dirpartscount=$(path_parts_count "$dir")
	if ((dirpartscount >= startpartscount)); then
		[[ -n $STARTDIR && $dir < $STARTDIR ]] && return 0
	fi
	local d
	for d in $SKIPDIR; do
		[[ $d == "$dir" ]] && return 0
	done
	return 1
}

############################################################
# List of well-known top-level port categories
# in a form usable in pattern matching: "foo|bar|baz"
#

top_level_categories=$(xargs <<EOF | sed -e 's/ /|/g'
archivers
astro
audio
base
benchmarks
biology
books
cad
chinese
comms
converters
databases
devel
editors
education
emulators
fonts
games
geo
graphics
inputmethods
japanese
java
korean
lang
mail
math
meta
misc
multimedia
net
news
perl5
plan9
print
productivity
security
shells
sysutils
telephony
tests
textproc
wayland
www
x11
EOF
)

############################################################
# List of variables that should not go into port Makefiles
#

user_settings=$(xargs <<EOF | sed -e 's/ /|/g'
BASELOCALSTATEDIR
BASESYSCONFDIR
BATCH
BUILD_ONCE
BULK
BULK_COOKIES_DIR
CHECKSUM_PACKAGES
CHECK_LIB_DEPENDS
COPTS
CXXOPTS
DISTDIR
ECHO_MSG
ECHO_REORDER
FAKEOBJDIR
FETCH_CMD
FETCH_PACKAGES
FETCH_SYMLINK_DISTFILES
FORCE_UPDATE
FTP_PACKAGES
IGNORE_IS_FATAL
IGNORE_SILENT
INTERACTIVE
LIST_DB
LOCKDIR
LOCK_VERBOSE
SITE_BACKUP
SITE_OVERRIDE
NO_CHECKSUM
NO_DEPENDS
NO_IGNORE
PACKAGE_REPOSITORY
PKG_ADD
PKG_CREATE
PKG_DBDIR
PKG_DELETE
PKG_INFO
REFETCH
SIGNING_PARAMETERS
SUDO
TEMPLATES
TRY_BROKEN
UNLOCK_CMD
UPDATE_COOKIES_DIR
USE_CCACHE
VARBASE
WARNINGS
EOF
)

############################################################
# 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
	skip_dir "$dir" && return
	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
			;;

		crates.inc|modules.inc)
			test -f "$F" || err "$F is not a file"
			fgrep -qx ".include \"$F\"" "$dir"/Makefile ||
				err "$F not included in Makefile"
			((++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 categories dist_subdir distfiles flavor flavors
	local gh_commit sites
	local multi_packages pkgpath_this pseudo_flavor pseudo_flavors
	local shared_libs subst_cmd
	local permit_package permit_distfiles
	local show_items="CATEGORIES DIST_SUBDIR DISTFILES FLAVOR FLAVORS GH_COMMIT"
	local show_items="$show_items 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 PERMIT_DISTFILES"
	local read_ok=false

	local read_failed=false
	(cd -- "$dir"; make "${make_args[@]}" show="$show_items" || true) </dev/null |&
	read -pr categories &&
	read -pr dist_subdir &&
	read -pr distfiles &&
	read -pr flavor &&
	read -pr flavors &&
	read -pr gh_commit &&
	read -pr 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 permit_package &&
	read -pr permit_distfiles &&
	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_categories "$dir" $categories
	check_distfiles "$dir" "$dist_subdir" $distfiles
	check_sites "$dir" $sites
	check_permit_dist "$dir" "$permit_package" "$permit_distfiles"
	$pkg_exists && check_pkg_dir "$dir"/pkg "$subst_cmd"
	$existing_port || check_shlibs_versions "$dir" $shared_libs

	if [[ -n $gh_commit ]]; then
		local ghclen=$(echo -n "$gh_commit" | wc -c)
		if ((ghclen != 40)); then
			err "GH_COMMIT should be in full form (40 characters)"
		fi
	fi

	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_dos_line_endings() {
	grep -q "$(printf '\r\n')\$" "$1" &&
		err "MS-DOS line endings in $1 -- if patching,"\
		"use FIX_CRLF_FILES to correct the input file."
}

# 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:
#   * All top-level category names are well-known.
check_categories() {
	$debugging && echo "CALLED: check_categories($*)" >&2

	local dir=$1; shift
	local portref=$(portref "$dir")
	for c in "$@"; do
		c=${c%%/*}
		if ! echo "$c" | egrep -q "^($top_level_categories)\$"; then
			err "${portref}non-standard top-level category: $c"
		fi
	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 SITES.
check_sites() {
	$debugging && echo "CALLED: check_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 WANTLIB-"
			vars="$vars PERMIT_PACKAGE${subpkg%-}"
			vars="$vars COMPILER"
			make "${make_args[@]}" show="$vars" | {
				local comment fullpkgname modules pkg_arch
				local wantlib permit_package
				local arch_independent=false
				local default_compiler=false
				read -r comment
				read -r fullpkgname
				read -r modules
				read -r pkg_arch
				[[ $pkg_arch == "*" ]] && arch_independent=true
				read -r wantlib
				read -r wantlib_ss
				read -r permit_package
				read -r compiler

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

				if $arch_independent && [[ -n $wantlib ]]; then
					if [[ $subpkg != - || -n $wantlib_ss ]]; then
						err "${portref}non-empty $wantlib_var for" \
						    "arch-independent package"
					fi
				fi

				[[ "$compiler" == "base-clang base-gcc" ]] \
				    && default_compiler=true

				check_wantlib "$portref" "$modules" \
				    "$default_compiler" $wantlib

				check_permit_subpkg "$portref" "$subpkg" \
				    "$permit_package"

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

				! $error
			} || error=true

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

# Checks made:
#   * If package installs system-wide icons, it should have the
#     x11/gtk+4,-guic dependency and 
#     @tag gtk-update-icon-cache %D/share/icons/$theme
#     for each icon theme used in package. 
#
#   * If package adds a MIME type handler, it should have the
#     devel/desktop-file-utils dependency and @tag 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 @tag update-mime-database
#
#   * If package adds a GLib schema, it should have 
#     @tag glib-compile-schemas
#     and "devel/dconf" in MODULES (or at least RDEP on devel/dconf).
#
#   * If package installs files under ${PREFIX}/share/dbus-1/system-services/,
#     it must have a run-time dependency on x11/dbus,-suid.
#
#   * 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}.
#
#   * Manual pages shouldn't be compressed.
#
#   * If port installs QML files, it should depend on
#     x11/qt5/qtdeclarative,-main
#
#   * Qt5-QML files should live under PREFIX/lib/qt5/qml/.
#
#   * Qt5-QML-enabled port depend on x11/qt5/qtdeclarative.
#
#   * @rcscript items and pkg-readmes have @mode, @owner and @group reset.
#
#   * PKG_ARCH is not set to * if a library is present.
#
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 arch_independent=$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 mime_dep=false
	local mime_dep_needed=false
	local mime_tag_cnt=0

	local mimepkg_dep=false
	local mimepkg_dep_needed=false

	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 dbus_suid_dep=false is_dbus_suid=false
	local dbus_suid_dep_needed=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

	local regsh=false unregsh=false
	local regsh_exec=false unregsh_exec=false

	local qml_found=false qt5_qml_found=false non_qt5_qml_found=false
	local qt5_dep=false qt5declarative_dep=false
	local lib_found=false

	local owner_set=false group_set=false mode_set=false
	local readme_seen=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)))
			# Themes have at least two levels in depth.
			#
			guic_dep_needed=true
			;;
		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/default.*)
			;;
		share/icons/*.@(png|svg|svgz|icon|ico))
			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+4,-guic"*)
			guic_dep=true
			;;
		@?(un)exec*" %D/bin/gtk-update-icon-cache"*)
			err "${portref}obsolete gtk-update-icon-cache" \
			    "invocation: ${l#@* }"
			;;

		share/applications/*(*/)*.desktop)
			mime_dep_needed=true
			;;
		"@depend devel/desktop-file-utils"*)
			mime_dep=true
			;;
		@?(un)exec*" %D/bin/update-desktop-database"*)
			err "${portref}obsolete update-desktop-database" \
			    "invocation: ${l#@* }"
			;;
		share/mime/packages/*.xml)
			mimepkg_dep_needed=true
			;;
		"@depend misc/shared-mime-info"*)
			mimepkg_dep=true
			;;
		@?(un)exec*" %D/bin/update-mime-database"*)
			err "${portref}obsolete update-mime-database" \
			    "invocation: ${l#@* }"
			;;

		share/glib-2.0/schemas/*.xml)
			dconf_dep_needed=true
			;;
		"@depend devel/dconf"*)
			dconf_dep=true
			;;
		@?(un)exec*" %D/bin/glib-compile-schemas"*)
			err "${portref}obsolete glib-compile-schemas" \
			    "invocation: ${l#@* }"
			;;

		"@depend x11/qt5/qtdeclarative,-main"*)
			qt5declarative_dep=true
			qt5_dep=true
			;;
		"@depend x11/qt5/"*)
			qt5_dep=true
			;;
		lib/qt5/qml/*)
			qml_found=true
			lib_found=true
			qt5_qml_found=true
			;;
		*/qmldir)
			qml_found=true
			non_qt5_qml_found=true
			;;

		*.so|*.a|*.so.*)
			lib_found=true
			;;

		share/dbus-1/system-services/*)
			dbus_suid_dep_needed=true
			;;
		"@depend x11/dbus,-suid"*)
			dbus_suid_dep=true
			;;
		"@bin libexec/dbus-daemon-launch-helper")
			is_dbus_suid=true
			;;

		lib/ghc/*/register.sh)
			regsh=true
			;;
		lib/ghc/*/unregister.sh)
			unregsh=true
			;;
		"@exec /usr/bin/env HOME=/nonexistent "%D/lib/ghc/*/register.sh" -v0")
			regsh_exec=true
			;;
		"@unexec /usr/bin/env HOME=/nonexistent "%D/lib/ghc/*/unregister.sh" -v0 --force")
			unregsh_exec=true
			if $unregsh; then
				err "${portref}unregister.sh call placed before" \
				    "script itself in PLIST${subpkg}"
			fi
			;;

		# 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
			;;
		"@man "*.gz)
			err "${portref}compressed $l"
			;;

		@owner)
			owner_set=false
			;;
		@group)
			group_set=false
			;;
		@mode)
			mode_set=false
			;;
		"@owner "*)
			owner_set=true
			;;
		"@group "*)
			group_set=true
			;;
		"@mode "*)
			mode_set=true
			;;
		"@rcscript "*)
			! $owner_set || err "${portref}$l has @owner set"
			! $group_set || err "${portref}$l has @group set"
			! $mode_set  || err "${portref}$l has @mode set"
			;;
		share/doc/pkg-readmes/*)
			! $owner_set || err "${portref}$l has @owner set"
			! $group_set || err "${portref}$l has @group set"
			! $mode_set  || err "${portref}$l has @mode set"
			readme_seen=true
			;;
		esac
	done

	# gtk-update-icon-cache
	$guic_dep_needed && ! $guic_dep &&
	    [[ $fullpkgname != gtk-update-icon-cache-* ]] &&
		err "${portref}missing" \
		    "RUN_DEPENDS${subpkg}+=x11/gtk+4,-guic"
	local cnt

	# desktop-file-utils (simplier than previous, isn't it?)
	$mime_dep_needed && ! $mime_dep &&
	    [[ $fullpkgname != desktop-file-utils-* ]] &&
		err "${portref}missing" \
		    "RUN_DEPENDS${subpkg}+=devel/desktop-file-utils"

	# update-mime-database (same as previous)
	$mimepkg_dep_needed && ! $mimepkg_dep &&
	    [[ $fullpkgname != shared-mime-info-* ]] &&
		err "${portref}missing" \
		    "RUN_DEPENDS${subpkg}+=misc/shared-mime-info"

	# 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

	# QML
	$qt5_qml_found && ! $qt5declarative_dep &&
		[[ $fullpkgname != qtdeclarative-[0-9]* ]] &&
		err "${portref}provides Qt5 QML files without" \
		    "x11/qt5/qtdeclarative dependency"
	$qt5_dep && $non_qt5_qml_found &&
		err "${portref}depends on Qt5 but installs QML files" \
		    "outside PREFIX/lib/qt5/qml/"

	$lib_found && $arch_independent &&
		err "${portref}arch-independent package contains library files"

	# dbus,-suid
	if $dbus_suid_dep_needed && ! $dbus_suid_dep && ! $is_dbus_suid; then
		err "${portref}missing" \
		    "RUN_DEPENDS${subpkg}+=x11/dbus,-suid"
	fi

	# Haskell register/unregister
	if $regsh && ! $regsh_exec; then
		err "${portref}missing @exec of register.sh"
	fi
	if $unregsh && ! $unregsh_exec; then
		err "${portref}missing @unexec of unregister.sh"
	fi

	# 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_COMPILEALL}: $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/"

	if ! $readme_seen; then
		local readme=pkg/README${subpkg}
		if [[ -e $readme ]]; then
			err "${portref}missing share/doc/pkg-readmes/\${PKGSTEM} in PLIST${subpkg}"
		fi
	fi
}

# Checks made:
#   * stdc++ doesn't get into WANTLIB when gcc4.port.mk is used.
check_wantlib() {
	local portref="$1"; shift
	local modules="$1"; shift
	local default_compiler="$1"; shift

	local gcc4_module=false

	local v

	for v in $modules; do case $v in
		gcc4)			gcc4_module=true;;
	esac; done

	for v; do case $v in
		@(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!)"
			;;

		@(c++|stdc++)?(?('>')=+([0-9])))
			if $default_compiler; then
				err "C++ libraries in WANTLIB with default COMPILER" \
				    "(most ports need 'COMPILER=base-clang ports-gcc'" \
				    "or 'COMPILER=base-clang ports-gcc base-gcc')"
			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

	true
}

# Checks made:
#  * Each library mentioned in WANTLIB is accessible either:
#    a) as a part of base system, in /usr/lib or /usr/X11R6/lib;
#    b) via LIB_DEPENDS directly, or via deeper dependency of LIB_DEPENDS.
check_lib_depends() {
	$debugging && echo "CALLED: check_lib_depends($*)" >&2

	local portref="$1"; shift
	local subpkg="$1"; shift
	local modules="$1"; shift
	local wantlib="$1"; shift

	# The idea as follows: build full list of run-time dependencies, but
	# without RUN_DEPENDS begin involved.
	#
	# Then we look at libs in each pkgpath we got, and strip those
	# from WANTLIB. We also strip system libraries from /usr/lib
	# and /usr/X11R6/lib. And all WANTLIBs coming from MODULES are stripped
	# too, supposing that authors of those MODULES know what they're doing
	# (without stripping 'em, we'll get many false positives).
	#
	# If there are any non-stripped items in WANTLIB, we found a problem.
	#
	# XXX those checks do not take actual versions into account!

	# get list of all WANTLIBs coming from MODULES
	local m modvars=
	for m in $modules; do
		m=${m##*/}
		case $m in
		python)
			m=py
			;;
		esac
		m=$(echo "MOD${m}_WANTLIB" | tr a-z A-Z)
		modvars="$modvars $m"
	done
	local l modlibs=
	make "${make_args[@]}" show="$modvars" </dev/null |&
	while read -pr l; do
		modlibs="$modlibs $l"
	done
	wait    # make sure process exited before possible return below

	# strip WANTLIBs coming from MODULES
	local libsleft wl
	for l in $modlibs; do
		libsleft=
		for wl in $wantlib; do
			if [[ $l != "$wl" ]]; then
				libsleft="$libsleft $wl"
			elif $debugging; then
				echo "WANTLIB ITEM $wl COMES FROM MODULES"
			fi
		done
		[[ -n $libsleft ]] || return 0    # all libs found
		wantlib=$libsleft
	done

	# prepare easy-to-use WANTLIB list in $checklibs
	local wlprefix checklibs=
	for wl in $wantlib; do
		wl=${wl%%[><=]*}
		wlprefix=${wl%/*}
		[[ $wlprefix == "$wl" ]] && wlprefix=lib
		wl=${wl##*/}
		checklibs="$checklibs ${wlprefix}/lib$wl"
	done

	# strip system libraries
	local d
	for d in /usr /usr/X11R6; do
		for l in $d/lib/lib*.@(so*(.+([0-9]))|a); do
			libsleft=
			for wl in $checklibs; do
				if [[ $l != +(/*)/${wl}.@(so*(.+([0-9]))|a) ]]; then
					libsleft="$libsleft $wl"
				elif $debugging; then
					echo "FOUND WANTLIB ITEM $wl: $l"
				fi
			done
			[[ -n $libsleft ]] || return 0    # all libs found
			checklibs=$libsleft
		done
	done

	# get deep list of LDEPs
	local lmake_args="${make_args[@]}"
	lmake_args[${#lmake_args[@]}]="RUN_DEPENDS="
	lmake_args[${#lmake_args[@]}]="RUN_DEPENDS$subpkg="
	# Rely on the fact we're already in the port directory, see sub_checks().
	# XXX ignoring make errors for now
	local pure_lib_deps=$(make "${lmake_args[@]}" show-run-depends | sort)
	[[ -n $pure_lib_deps ]] || return 0
	# SUBDIR doesn't accept newline-separated values
	set -A pure_lib_deps -- $pure_lib_deps

	(
	# strip libraries from ports
	# TODO cache print-plist-libs output?
	cd -- /usr/ports    # XXX "$portsdir" fails for openbsd-wip and like
	unset FLAVOR SUBPACKAGE
	SUBDIR="${pure_lib_deps[*]}" make "${make_args[@]}" \
	    print-plist-libs </dev/null 2>/dev/null |&
	while read -pr l; do
		case $l in
		"===> "*)
			;;

		*)
			libsleft=
			for wl in $checklibs; do
				if [[ $l != +(/*)/${wl}.@(so*(.+([0-9]))|a) ]]; then
					libsleft="$libsleft $wl"
				elif $debugging; then
					echo "FOUND WANTLIB ITEM $wl: $l"
				fi
			done
			[[ -n $libsleft ]] || exit 0    # all libs found
			checklibs=$libsleft
			;;
		esac
	done

	# prettify list of WANTLIBs left and print it
	libsleft=
	for wl in $checklibs; do
		libsleft="$libsleft ${wl##*/lib}"
	done
	err "${portref}the following libraries in WANTLIB${subpkg%-}" \
	    "look like masked by RUN_DEPENDS${subpkg%-}:$libsleft"
	wait
	! $error
	) || error=true
}

# Checks made:
#   * No extra PERMIT_DISTFILES variables in Makefile.
#   * PERMIT_DISTFILES 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 permit_package=$(echo "$1" | tr '[:upper:]' '[:lower:]')
	local permit_distfiles=$(echo "$2" | tr '[:upper:]' '[:lower:]')

	if [[ $permit_package == yes && $permit_distfiles == yes ]]; then
		egrep -sq "^ *PERMIT_DISTFILES[[:space:]]*=" Makefile &&
			err "${portref}extra PERMIT_DISTFILES line(-s)"
	fi

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

	true
}

# Checks made:
#   * 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 permit_package=$(echo "$1" | tr '[:upper:]' '[:lower:]')

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

	true
}

# Checks made:
#   * Directory is not empty
#   * No '*.core' files present
#   * Files should not contain OpenBSD RCS tags
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
			if [[ $F == *.core ]]; then
				err_coredump_found "$F"
			else
				grep -q '\$OpenBSD.*\$' "$F" &&
					err "$F should not contain \$OpenBSD\$ tag"
			fi
		done
		$empty && err "there are no files, please remove the $1 directory"
		! $error
	} || error=true
}

# Checks made:
#   * The patch is not empty.
#   * The patch should not contain an OpenBSD RCS tag.
check_patch() {
	local F=$1

	test -f "$F" || {
		err "$F is not a file"
		return
	}

	if [[ -s $F ]]; then
		grep -q '\$OpenBSD.*\$' "$F" &&
		    err "$F should not contain \$OpenBSD\$ tag"
	else
		err "$F is empty and should be removed"
	fi

	check_dos_line_endings "$F"
}

# Checks made:
#   * Patches should not contain OpenBSD RCS tags.
#   * 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
		$rootrun || check_patch "$F"
		;;

	*)
		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.
#   * Files should not contain OpenBSD RCS tags.
#   * PFRAG.shared should be merged into PLIST.
#   * 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
	local plist

	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"
		grep -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"
		plist=PLIST${F##*/PFRAG.+([!-])}
		err "$F should be merged into $plist"
		;;

	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"
		grep -q '\$OpenBSD.*\$' "$F" &&
			err "$F should not contain \$OpenBSD\$ tag"
		;;

	*.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"
		check_rcscript "$dir" "${F##*/}"
		grep -q '\$OpenBSD.*\$' "$F" &&
			err "$F should not contain \$OpenBSD\$ tag"
		;;

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

# Checks made:
#   * All pkg/foo.rc files are found in some PLIST* or PFRAG*.
check_rcscript() {
	$debugging && echo "CALLED: check_rcscript($*)" >&2
	local dir="$1" rcname="$2"

	fgrep -xsq "@rcscript \${RCDIR}/${rcname%.rc}" \
	    "$dir"/PLIST* "$dir"/PFRAG* || err \
		"$F is not mentioned in any packing list"
}

# 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:
#   * Contains no OpenBSD RCS tag.
#   * 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"
	grep -q '\$OpenBSD.*\$' "$1" &&
		err "$1 should not contain \$OpenBSD\$ tag"

	# 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 no OpenBSD RCS tag.
#   * No REVISION marks present in given file (unless in update mode).
#   * Each REVISION mark presents only once.
#   * BUILD_DEPENDS, MODULES and PERMIT_DISTFILES 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 statements.
#   * No user settings present.
#   * SHARED_ONLY not defined
#   * Check for usage of obsolete PERMIT_PACKAGE_* and PERMIT_DISTFILES_FTP
check_makefile() {
	$debugging && echo "CALLED: check_makefile($*)" >&2

	local F="$1"
	check_trailing_whitespace "$F"
	check_long_lines "$F"
	check_hardcoded "$F"
	grep -q '\$OpenBSD.*\$' "$F" &&
		err "$F should not contain \$OpenBSD\$ tag"

	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

		if echo "$l" | egrep -q "^ *($user_settings)[[:>:]].*\$"; then
			err "user settings in port: $l"
		fi

		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)-*)
			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
			;;
		*(" ")SHARED_ONLY*(" "|"$tab")*(+|:|!|\?)=*)
			err "SHARED_ONLY is deprecated ($F:$lnum)"
			;;
		*(" ")PERMIT_PACKAGE_CDROM*(" "|"$tab")*(+|:|!|\?)=*)
			err "PERMIT_PACKAGE_CDROM is deprecated," \
			    "use PERMIT_PACKAGE ($F:$lnum)"
			;;
		*(" ")PERMIT_PACKAGE_FTP*(" "|"$tab")*(+|:|!|\?)=*)
			err "PERMIT_PACKAGE_FTP is deprecated," \
			    "use PERMIT_PACKAGE ($F:$lnum)"
			;;
		*(" ")PERMIT_DISTFILES_FTP*(" "|"$tab")*(+|:|!|\?)=*)
			err "PERMIT_DISTFILES_FTP is deprecated," \
			    "use PERMIT_DISTFILES ($F:$lnum)"
			;;
		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
