# Copyright 2024 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

# @ECLASS: gap-pkg.eclass
# @MAINTAINER:
# François Bissey <frp.bissey@gmail.com>
# Michael Orlitzky <mjo@gentoo.org>
# Gentoo Mathematics Project <sci-mathematics@gentoo.org>
# @AUTHOR:
# François Bissey <frp.bissey@gmail.com>
# Michael Orlitzky <mjo@gentoo.org>
# @SUPPORTED_EAPIS: 8
# @BLURB: Simplify the installation of GAP packages.
# @DESCRIPTION:
# The main purpose of this eclass is to build and install GAP packages
# that typically occupy the dev-gap category. Most GAP packages do not
# support an install target out of the box, so the default installation
# is "by hand," with attention paid to those directories that are part
# of the recommended layout. The prepare, configure, and compile phases
# do however try to support packages having a real build system.
#
# GAP itself has four "required" packages that are packaged separately,
# making dependencies between them somewhat weird. The four required
# packages are,
#
#   * dev-gap/gapdoc
#   * dev-gap/primgrp
#   * dev-gap/smallgrp
#   * dev-gap/transgrp
#
# Those four packages will have only sci-mathematics/gap added to
# RDEPEND. All other packages will have the four required packages above
# added to RDEPEND in addition to sci-mathematics/gap. In theory it
# would be better to list all dependencies explicitly rather than
# grouping together the "required" four, but this is how upstream GAP
# works, and is what all GAP packages expect; for example, most test
# suites fail without the required packages but make no attempt to load
# them.
#
# If you need a version constraint on sci-mathematics/gap, you'll have
# to specify it yourself. Compiled packages will likely need
# sci-mathematics/gap in DEPEND as well, and may also want a subslot
# dependency.

case ${EAPI} in
	8) ;;
	*) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;;
esac

# For eshopts_push and eshopts_pop
inherit estack

# Some packages have additional homepages, but pretty much every GAP
# package can be found at this URL.
HOMEPAGE="https://www.gap-system.org/Packages/${PN}.html"

# _GAP_PKG_IS_REQUIRED is an internal variable that indicates whether or
# not $PN is one of the four "required" GAP packages that are always
# loaded, even when GAP is started with the "-A" flag. We treat this
# four somewhat differently since they are implicit dependencies of
# everything else in the GAP ecosystem.
_GAP_PKG_IS_REQUIRED=no
case ${CATEGORY}/${PN} in
	 dev-gap/gapdoc|dev-gap/smallgrp|dev-gap/primgrp|dev-gap/transgrp)
		_GAP_PKG_IS_REQUIRED=yes
		;;
	 *)
		;;
esac

# _GAP_PKG_RDEPEND is an internal variable to hold the RDEPEND entries
# added by this eclass. We use a separate variable for this because we
# need its contents later in gap-pkg_enable_tests, and that function is
# called from an ebuild context where the list of RDEPEND is maintained
# separately. Basically: the values we add to RDEPEND here do not appear
# in RDEPEND when gap-pkg_enable_tests is called.
_GAP_PKG_RDEPEND="sci-mathematics/gap"

# The four "required" packages depend only on GAP itself, while every
# other package depends (also) on the four required ones.
if [[ "${_GAP_PKG_IS_REQUIRED}" = "no" ]]; then
	_GAP_PKG_RDEPEND+="
		dev-gap/gapdoc
		dev-gap/smallgrp
		dev-gap/primgrp
		dev-gap/transgrp"
fi
RDEPEND="${_GAP_PKG_RDEPEND}"

# @FUNCTION: gap-pkg_dir
# @DESCRIPTION:
# The directory into which the gap package should be installed. The
# accepted current location is /usr/$(get_libdir)/gap/pkg, but
# technically this depends on the econf call in sci-mathematics/gap.
gap-pkg_dir() {
	echo "/usr/$(get_libdir)/gap/pkg/${PN}"
}

# @FUNCTION: _gap-pkg_gaproot
# @INTERNAL
# @DESCRIPTION:
# The directory containing sysinfo.gap. This is frequently passed to GAP
# packages via ./configure --with-gaproot or as a positional argument to
# hand-written configure scripts. We also use it to find the value of
# $GAParch, which is contained in sysinfo.gap. The "gaproot" is
# implicitly determined by the econf call in sci-mathematics/gap. As a
# result, calling this function requires sci-mathematics/gap at
# build-time.
_gap-pkg_gaproot() {
	echo "${ESYSROOT}/usr/$(get_libdir)/gap"
}

# @FUNCTION: gap-pkg_econf
# @USAGE: [extra econf args]
# @DESCRIPTION:
# Call econf, passing the value of _gap-pkg_gaproot to --with-gaproot.
# All arguments to gap-pkg_econf are passed through to econf.
#
# @EXAMPLE
# src_configure() {
# 	gap-pkg_econf --with-external-libsemigroups
# }
#
gap-pkg_econf() {
	econf --with-gaproot="$(_gap-pkg_gaproot)" "${@}"
}

# @FUNCTION: gap-pkg_src_configure
# @DESCRIPTION:
# Handle both autoconf configure scripts and the hand-written ones used
# by many GAP packages. We determine which one we're dealing with by
# running ./configure --help; an autoconf configure script will mention
# "PREFIX" in the output, the others will not.
#
# Autoconf configure scripts are configured using gap-pkg_econf, while
# hand-written ones are executed directly with _gap-pkg_gaproot as their
# sole positional argument.
gap-pkg_src_configure() {
	local _configure="${ECONF_SOURCE:-.}/configure"
	if [[ -x ${_configure} ]] ; then
		if ${_configure} --help | grep PREFIX &>/dev/null; then
			# This is an autoconf ./configure script
			gap-pkg_econf
		else
			# It's an "old-style" handwritten script that does
			# not print usage information with --help.
			${_configure} $(_gap-pkg_gaproot) || die
		fi
	fi
}

# @FUNCTION: gap-pkg_src_compile
# @DESCRIPTION:
# The default src_compile with the addition of V=1 to emake. The
# Makefile.gappkg used to build most C packages defaults to a quiet
# build without this.
gap-pkg_src_compile() {
	if [[ -f Makefile ]] || [[ -f GNUmakefile ]] || [[ -f makefile ]]; then
		emake V=1 || die "emake failed"
	fi
}

# @FUNCTION: gap-pkg_enable_tests
# @DESCRIPTION:
# Amend IUSE, RESTRICT, and BDEPEND for a package with a test suite.
# This is modeled on similar functions in the distutils-r1 and
# elisp-common eclasses, except here only a single default testing
# strategy is supported. All runtime and post-merge dependencies are
# added as build dependencies if USE=test is set.
gap-pkg_enable_tests() {
	IUSE+=" test "
	RESTRICT+=" !test? ( test ) "

	# Use the internal variable here, too, because the RDEPEND list from
	# the ebuild is maintained separately by the package manager. We add
	# PDEPEND too because we use it to break some circular dependencies
	# between e.g. polycyclic and alnuth.
	BDEPEND+=" test? ( ${_GAP_PKG_RDEPEND} ${RDEPEND} ${PDEPEND} ) "
}

# @FUNCTION: gap-pkg_src_test
# @DESCRIPTION:
# Run this package's test suite if it has one. The GAP TestPackage
# function is the standard way to do this, but it does rely on the
# package itself to get a few things right, like running the tests
# verbosely and exiting with the appropriate code. The alternative would
# be run TestDirectory ourselves on "tst", but that has its own issues;
# in particular many packages have set-up code that is run only with
# TestPackage. YMMV.
gap-pkg_src_test() {
	[[ -f PackageInfo.g ]] || return

	# We would prefer --bare to -A so that we can test (say) primgrp
	# after installing only gapdoc and not smallgrp or transgrp. But,
	# that would cause problems for basically every non-required
	# package, because they usually don't explicitly load the four
	# "required" packages in their test suites. So we use -A unless
	# this is one of the chosen four.
	local bareflag="--bare"
	if [[ "${_GAP_PKG_IS_REQUIRED}" = "no" ]]; then
		bareflag="-A"
	fi

	# Run GAP non-interactively to test the just-built package. We omit
	# the "-r" flag here because we use the UserGapRoot directory to
	# store AtlasRep data, and without it, the atlasrep tests (and the
	# tests of any packages depending on it) will fail.
	local gapcmd="gap -R ${bareflag} --nointeract"

	# ForceQuitGap translates a boolean return value to the expected
	# zero or one, useful for packages that set a single *.tst file as
	# their TestFile.
	gapcmd+=" -c ForceQuitGap(TestPackage(\"${PN}\"));"

	# Fake the directory structure that GAP needs to be able to find
	# packages with a symlink under ${T}, then prepend ${T} to the list
	# of search paths so that if this package is already installed, we
	# load the just-built copy first.
	ln -s "${WORKDIR}" "${T}/pkg" || die
	gapcmd+=" --roots ${T}/; "

	# False negatives can occur if GAP fails to start, or if there are
	# syntax errors:
	#
	#   https://github.com/gap-system/gap/issues/5541
	#
	# There's nothing you can do about that, but now you know.
	#
	# The pipe to tee is more important than it looks. Any test suite
	# involving dev-gap/browse is likely to bork the user's terminal.
	# The "browse" package is however smart enough to figure out when
	# stdout is not a tty, and avoids breaking it in that case. So by
	# piping to tee, we encourage it not to do anything too crazy.
	eshopts_push -o pipefail
	${gapcmd} | tee test-suite.log \
		|| die "test suite failed, see test-suite.log"
	eshopts_pop
}

# @ECLASS_VARIABLE: GAP_PKG_EXTRA_INSTALL
# @DEFAULT_UNSET
# @DESCRIPTION:
# A bash array of extra files and directories to install recursively at
# the root of this package's directory tree. For example, if you have a
# package that mostly follows the suggested layout (described in the
# gap-pkg_src_install documentation) but also includes a "data"
# directory, you should set
#
#   GAP_PKG_EXTRA_INSTALL=( data )
#
# to install the data directory without having to override the entire
# src_install phase.

# @ECLASS_VARIABLE: GAP_PKG_HTML_DOCDIR
# @DESCRIPTION:
# The directory inside the tarball where the HTML documentation is
# located. This is _usually_ "doc", which conforms to the suggested
# GAPDoc layout and is the default value of this variable. Many
# packages however use a top-level "htm" directory instead. The named
# directory will be installed to gap-pkg_dir and symlinked to the usual
# location under /usr/share/doc. As a result, you should only use this
# for directories referenced by PackageInfo.g or by some other part of
# the package. HTML documentation whose location doesn't need to be
# known to the package at runtime should instead be installed with
# HTML_DOCS or a similar mechanism.
: "${GAP_PKG_HTML_DOCDIR:=doc}"

# @FUNCTION: gap-pkg_src_install
# @DESCRIPTION:
# Install a GAP package that follows the suggested layout,
#
#   https://docs.gap-system.org/doc/ref/chap76.html
#
# In particular:
#
# 1. All GAP source files (*.g) in $S are installed.
#
# 2. If a library directory named "gap" or "lib" exists,
#    it is installed.
#
# 3. If a binary directory "bin" exists, it is installed.
#
# 4. If a "doc" directory exists, we assume GAPDoc conventions
#    (https://docs.gap-system.org/pkg/gapdoc/doc/chap5.html) and install
#    what we find there. Unfortunately for us, each package's
#    PackageInfo.g contains a "PackageDoc" section that points to this
#    documentation, and we can't break the paths it references. Instead,
#    we try to dosym the human-readable parts of the documentation (PDF
#    manuals) into appropriate Gentoo locations.
#
# 5. We consult GAP_PKG_HTML_DOCDIR for the HTML documentation and repeat
#    the process above.
#
# A few GAP packages have autotools build systems with working "make
# install" routines, but most don't. So for the time being we omit that
# step. It's harder to work around the packages that don't support it
# than the other way around.
gap-pkg_src_install() {
	einstalldocs

	# So we don't have to "test -f" on the result of every glob.
	eshopts_push -s nullglob

	# Install the "normal" documentation from the doc directory. This
	# includes anything the interactive GAP help might need in addition
	# to the documentation intended for direct user consumption.
	if [[ -d doc ]]; then
		pushd doc > /dev/null || die

		local docdir="$(gap-pkg_dir)/doc"
		insinto "${docdir}"

		# These files are needed by the GAP interface. We don't symlink
		# these because they're not meant for direct human consumption;
		# the text files are not *plain* text -- they contain color
		# codes. I'm not sure if the BibTeX files are actually used,
		# but the GAP packaging documentation mentions specifically
		# that they should be included. XML files are included in case
		# the bibliography is in BibXMLext format, but you may wind up
		# with some additional GAPDoc (XML) source files as a result.
		for f in *.{bib,lab,six,tex,txt,xml}; do
			doins "${f}"
		done

		# The PDF docs are also potentially used by the interface, since
		# they appear in PackageInfo.g, so we install them "as is." But
		# then afterwards we symlink them to their proper Gentoo
		# locations
		for f in *.pdf; do
			doins "${f}"
			dosym -r "${docdir}/${f}" "/usr/share/doc/${PF}/${f}"
		done

		popd > /dev/null || die
	fi

	# Install the HTML documentation. The procedure is basically the
	# same as for the PDF docs.
	if [[ -d "${GAP_PKG_HTML_DOCDIR}" ]]; then
		pushd "${GAP_PKG_HTML_DOCDIR}" > /dev/null || die

		local docdir="$(gap-pkg_dir)/${GAP_PKG_HTML_DOCDIR}"
		insinto "${docdir}"

		# See above
		for f in *.{htm,html,css,js,png}; do
			doins "${f}"
			dosym -r "${docdir}/${f}" "/usr/share/doc/${PF}/html/${f}"
		done

		popd > /dev/null || die
	fi

	# Any GAP source files that live in the top-level directory.
	insinto $(gap-pkg_dir)
	for f in *.g; do
		doins "${f}"
	done

	# We're done globbing
	eshopts_pop

	# The gap and lib dirs that usually also contain GAP code.
	[[ -d gap ]] && doins -r gap
	[[ -d lib ]] && doins -r lib

	# Any additional user-specified files or directories.
	for f in "${GAP_PKG_EXTRA_INSTALL[@]}"; do
		doins -r "${f}"
	done

	# The bin dir, that contains shared libraries but also sometimes
	# regular executables in an arch-specific subdirectory. We do
	# this last because it messes with insopts -- doexe doesn't work
	# recursively and we don't care what the subdirectory structure is.
	if [[ -d bin ]]; then
		insopts -m0755
		doins -r bin

		# Find and remove .la files from this package's bindir. The
		# usual "find" command doesn't work here because occasionally we
		# find *.la files in GAP packages that are not libtool archives
		# and should not be deleted.
		find "${ED%/}$(gap-pkg_dir)/bin" -type f -name '*.la' -delete || die
	fi
}

EXPORT_FUNCTIONS src_configure src_compile src_test src_install