# Copyright 2019-2025 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

# QA checks related to site-packages directory:
# - files installed top-level instead of into site-packages
# - bad/unknown package versions
# - deprecated .egg files
# - stray files in top-level site-packages directory
# - forbidden top-level package names ("test" and so on)
# - extensions compiled for wrong Python implementation
# - missing, mismatched or stray .pyc files
# Maintainer: Python project <python@gentoo.org>

python_site_check() {
	local save=$(shopt -p nullglob)
	shopt -s nullglob
	local progs=( "${EPREFIX}"/usr/lib/python-exec/*/gpep517 )
	local bad_libdirs=()
	[[ $(get_libdir) != lib ]] && bad_libdirs=(
		"${ED}/usr/$(get_libdir)"/{python3,pypy}*
	)
	${save}

	local forbidden_package_names=(
		# NB: setuptools/discovery.py is a good source of ideas
		benchmark benchmarks dist doc docs examples scripts tasks
		test tests tools util utils
		# catch double-prefix installs, e.g. https://bugs.gentoo.org/618134
		lib $(get_libdir) usr
		.pytest_cache .hypothesis _trial_temp
	)

	local invalid=()
	local mismatched_timestamp=()
	local mismatched_data=()
	local missing=()
	local stray=()

	local bad_versions=()
	local eggs=()
	local namespace_packages=()
	local outside_site=()
	local stray_packages=()
	local wrong_ext=()

	# Avoid running the check if sufficiently new gpep517 is not installed
	# yet. It's valid to schedule (for merge order) >=gpep517-8 after
	# packages which have this check run if they don't use distutils-r1.
	if [[ ${EAPI} == [0123] ]] || ! nonfatal has_version ">=dev-python/gpep517-8" ; then
		return
	fi

	local f prog
	for prog in "${progs[@]}"; do
		local impl=${prog%/*}
		impl=${impl##*/}

		local pydir="${ED}"/usr/lib/${impl}
		[[ ${impl} == pypy3 ]] && pydir="${ED}"/usr/lib/pypy3.10
		[[ -d ${pydir} ]] || continue

		# check for packages installing outside site-packages
		case ${CATEGORY}/${PN} in
			dev-lang/pypy|dev-lang/python|dev-python/pypy*|dev-python/python-tests)
				;;
			*)
				while IFS= read -d $'\0' -r f; do
					outside_site+=( "${f}" )
				done < <(
					find "${pydir}" -mindepth 1 -maxdepth 1 \
						'!' -name site-packages -print0
				)
				;;
		esac

		local sitedir=( "${pydir}"/site-packages )
		[[ -d ${sitedir} ]] || continue

		local modname=$(
			"${impl}" - <<-EOF
				import sysconfig
				print(sysconfig.get_config_var("SHLIB_SUFFIX"))
			EOF
		)

		# check for bad package versions
		while IFS= read -d $'\0' -r f; do
			bad_versions+=( "${f#${ED}}" )
		done < <(
			find "${sitedir}" -maxdepth 1 '(' \
				-name '*-0.0.0.dist-info' -o \
				-name '*UNKNOWN*.dist-info' -o \
				-name '*-0.0.0.egg-info' -o \
				-name '*UNKNOWN*.egg-info' \
				')' -print0
		)

		# check for deprecated egg format
		while IFS= read -d $'\0' -r f; do
			eggs+=( "${f#${ED}}" )
		done < <(
			find "${sitedir}" -maxdepth 1 '(' \
				-name '*.egg-info' -o \
				-name '*.egg' \
				')' -print0
		)

		# check for stray files in site-packages
		while IFS= read -d $'\0' -r f; do
			stray_packages+=( "${f#${ED}}" )
		done < <(
			find "${sitedir}" -maxdepth 1 -type f '!' '(' \
					-name '*.egg' -o \
					-name '*.egg-info' -o \
					-name '*.pth' -o \
					-name '*.py' -o \
					-name '*.pyi' -o \
					-name "*${modname}" -o \
					-name 'README.txt' \
				')' -print0
		)

		# check for forbidden packages
		for f in "${forbidden_package_names[@]}"; do
			[[ -d ${sitedir}/${f} ]] && stray_packages+=(
				"${sitedir#${ED}}/${f}"
			)
		done

		# check for extensions compiled for wrong implementations
		local ext_suffix=$(
			"${impl}" - <<-EOF
				import sysconfig
				print(sysconfig.get_config_var("EXT_SUFFIX"))
			EOF
		)
		while IFS= read -d $'\0' -r f; do
			wrong_ext+=( "${f#${ED}}" )
		done < <(
			find "${sitedir}" '(' \
				-name "*.pypy*${modname}" -o \
				-name "*.cpython*${modname}" \
				')' -a '!' -name "*${ext_suffix}" -print0
		)

		# check for namespace packages
		while IFS= read -d $'\0' -r f; do
			if grep -q "pkg_resources.*declare_namespace" "${f}"
			then
				namespace_packages+=( "${f}" )
			fi
		done < <(
			find "${sitedir}" -maxdepth 2 -type f -name '__init__.py' -print0
		)

		einfo "Verifying compiled files for ${impl}"
		local kind pyc py
		while IFS=: read -r kind pyc py extra; do
			case ${kind} in
				invalid)
					invalid+=( "${pyc}" )
					;;
				mismatched)
					case ${extra} in
						timestamp)
							mismatched_timestamp+=( "${pyc}" )
							;;
						*)
							mismatched_data+=( "${pyc}" )
							;;
					esac
					;;
				missing)
					missing+=( "${pyc}" )
					;;
				older)
					# older warnings were produced by earlier version
					# of gpep517 but the check was incorrect, so we just
					# ignore them
					;;
				stray)
					stray+=( "${pyc}" )
					;;
			esac
		done < <("${prog}" verify-pyc --destdir "${D}" --prefix "${EPREFIX}"/usr)
	done

	local found=
	if [[ ${missing[@]} ]]; then
		eqawarn
		eqawarn "QA Notice: This package installs one or more Python modules that are"
		eqawarn "not byte-compiled."
		eqawarn "The following files are missing:"
		eqawarn
		eqatag -v python-site.pyc.missing "${missing[@]}"
		found=1
	fi

	if [[ ${invalid[@]} ]]; then
		eqawarn
		eqawarn "QA Notice: This package installs one or more compiled Python modules"
		eqawarn "that seem to be invalid (do not have the correct header)."
		eqawarn "The following files are invalid:"
		eqawarn
		eqatag -v python-site.pyc.invalid "${invalid[@]}"
		found=1
	fi

	if [[ ${mismatched_data[@]} ]]; then
		eqawarn
		eqawarn "QA Notice: This package installs one or more compiled Python modules whose"
		eqawarn ".py files have different content (size or hash) than recorded:"
		eqawarn
		eqatag -v python-site.pyc.mismatched.data "${mismatched_data[@]}"
		found=1
	fi

	if [[ ${mismatched_timestamp[@]} ]]; then
		eqawarn
		eqawarn "QA Notice: This package installs one or more compiled Python modules whose"
		eqawarn ".py files have different timestamps than recorded:"
		eqawarn
		eqatag -v python-site.pyc.mismatched.timestamp "${mismatched_timestamp[@]}"
		found=1
	fi

	if [[ ${stray[@]} ]]; then
		eqawarn
		eqawarn "QA Notice: This package installs one or more compiled Python modules"
		eqawarn "that do not match installed modules (or their implementation)."
		eqawarn "The following files are stray:"
		eqawarn
		eqatag -v python-site.pyc.stray "${stray[@]}"
		found=1
	fi

	if [[ ${found} ]]; then
		eqawarn
		eqawarn "For more information on bytecode files and related issues, please see:"
		eqawarn "  https://projects.gentoo.org/python/guide/qawarn.html#compiled-bytecode-related-warnings"
	fi

	if [[ ${bad_versions[@]} ]]; then
		eqawarn
		eqawarn "QA Notice: The following Python packages were installed with"
		eqawarn "invalid/suspicious names or versions in the site-packages directory:"
		eqawarn
		eqatag -v python-site.bad_version "${bad_versions[@]}"
	fi

	if [[ ${eggs[@]} ]]; then
		eqawarn
		eqawarn "QA Notice: The following deprecated .egg or .egg-info files were found."
		eqawarn "Please migrate the ebuild to use the PEP517 build."
		eqawarn
		eqatag -v python-site.egg "${eggs[@]}"
	fi

	if [[ ${stray_packages[@]} ]]; then
		eqawarn
		eqawarn "QA Notice: The following unexpected files/directories were found"
		eqawarn "top-level in the site-packages directory:"
		eqawarn
		eqatag -v python-site.stray "${stray_packages[@]}"
		eqawarn
		eqawarn "This is most likely a bug in the build system.  More information"
		eqawarn "can be found in the Python Guide:"
		eqawarn "https://projects.gentoo.org/python/guide/qawarn.html#stray-top-level-files-in-site-packages"
		# TODO: make this fatal once we fix the existing issues, and remove
		# the previous version from distutils-r1
		#die "Failing install because of stray top-level files in site-packages"
	fi

	if [[ ${bad_libdirs[@]} ]]; then
		eqawarn
		eqawarn "QA Notice: Package installs Python files to /usr/$(get_libdir)"
		eqawarn "instead of /usr/lib (use \$(python_get_sitedir)):"
		eqawarn
		eqatag -v python-site.libdir "${bad_libdirs[@]#${ED}}"
	fi

	if [[ ${namespace_packages[@]} ]]; then
		eqawarn
		eqawarn "QA Notice: Package installs pkg_resources namespace packages that"
		eqawarn "are deprecated.  Please either remove __init__.py in favor of PEP 420"
		eqawarn "implicit namespace, if possible, or replace it with pkgutil namespace."
		eqawarn
		eqatag -v python-site.libdir "${namespace_packages[@]#${ED}}"
		eqawarn
		eqawarn "Please report a bug upstream.  See also:"
		eqawarn "https://projects.gentoo.org/python/guide/concept.html#namespace-packages"
	fi

	if [[ ${outside_site[@]} ]]; then
		eqawarn
		eqawarn "QA Notice: Files found installed directly into Python stdlib,"
		eqawarn "instead of site-packages (use \$(python_get_sitedir)):"
		eqawarn
		eqatag -v python-site.stdlib "${outside_site[@]}"
	fi

	if [[ ${wrong_ext[@]} ]]; then
		eqawarn
		eqawarn "QA Notice: Extensions found compiled for the wrong Python version"
		eqawarn "(likely broken build isolation):"
		eqawarn
		eqatag -v python-site.wrong-ext "${wrong_ext[@]}"
	fi
}

python_site_check

: # guarantee successful exit

# vim:ft=ebuild
