Hi all,

I've been working with some mkdocs-* related ebuilds, and I kept having to do the same lines over and over again to get things to build the documentation. Therefore, I felt that it might be useful to automate this a bit with an eclass. However, because mkdocs is a relatively small, I tried to write a somewhat more generic eclass that supports multiple doc builders. So far I have added support for mkdocs, sphinx and doxygen.

The functions are based on the `distutils_enable_sphinx` function from the distutils-r1 eclass (but unlike the distutils function, sphinx building also works for non-python packages in the docs eclass). The generic parts have been stripped out and put into the generic part of the eclass. Each doc builder has it's own _setup function to set up the dependencies, and _compile function which calls the documentation builder and deals with things specific to that doc builder. The rest should be pretty self explanatory (I hope).

In my opinion it is very easy to use, e.g.:

```

DOCBUILDER="mkdocs" (or "sphinx" or "doxygen" )

DOCDEPEND="dev-python/mkdocs-material" (additional doc deps, apart from the doc builder itself)

inherit docs

```


There are some more variables which can be set for more advanced use cases (e.g. DOCDIR if the docs are not in ${S}), see the in-file documentation. And see https://gitweb.gentoo.org/repo/proj/guru.git/tree/dev-python/mkdocs-material/mkdocs-material-5.1.1.ebuild?h=dev for an example.


I'd be very interested to hear what you all think of this eclass, and if it would make a nice addition to ::gentoo? I'd also be interested to hear about any other documentation builders that might be added to the eclass (I tried to write it such that it is easy to add support for more doc builders). (Juippis already suggested to add doxygen yesterday in https://github.com/gentoo/gentoo/pull/15302 )

There are some mkdocs related ebuilds in the guru overlay https://github.com/gentoo/guru that this eclass can be tested on (e.g. mkdocs-material). Though a circular dependency prevents from setting USE="doc" on the entire overlay (mkdocs-material depends on things, whose documentation depends on mkdocs-material again), it is easily resolved by first emerging those packages with USE="-doc".

I already tested this in every use case I could think of, mkdocs/sphinx python ebuild, mkdocs non-python ebuild, doxygen ebuild, sphinx with and without autodoc, mkdocs with and without mkautodoc, docs in a subdir. And so far it seems to be working just fine. It's also been in the guru overlay for quite some time already.

Please let me know if you find any issues, or potential improvements.

Best Regards,

Andrew

See Also: https://github.com/gentoo/gentoo/pull/15302

# Copyright 1999-2020 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

# @ECLASS: docs.eclass
# @MAINTAINER:
# Andrew Ammerlaan <andrewammerl...@riseup.net>
# @AUTHOR:
# Author: Andrew Ammerlaan <andrewammerl...@riseup.net>
# Based on the work of: Michał Górny <mgo...@gentoo.org>
# @SUPPORTED_EAPIS: 5 6 7
# @BLURB: A simple eclass to build documentation.
# @DESCRIPTION:
# A simple eclass providing functions to build documentation.
#
# Please note that docs sets RDEPEND and DEPEND unconditionally
# for you.
#
# This eclass also appends "doc" to IUSE, and sets HTML_DOCS
# to the location of the compiled documentation
#
# The aim of this eclass is to make it easy to add additional
# doc builders. To do this, add a <DOCBUILDER>-setup and
# <DOCBUILDER>-build function for your doc builder.
# For python based doc builders you can use the 
# python_append_deps function to append [${PYTHON_USEDEP}]
# automatically to additional dependencies
#
# For more information, please see the Python Guide:
# https://dev.gentoo.org/~mgorny/python-guide/

case "${EAPI:-0}" in
        0|1|2|3|4)
                die "Unsupported EAPI=${EAPI:-0} (too old) for ${ECLASS}"
                ;;
        5|6|7)
                ;;
        *)
                die "Unsupported EAPI=${EAPI} (unknown) for ${ECLASS}"
                ;;
esac

# @ECLASS-VARIABLE: DOCBUILDER
# @REQUIRED
# @PRE_INHERIT
# @DESCRIPTION:
# Sets the doc builder to use, currently supports
# sphinx and mkdocs

# @ECLASS-VARIABLE: DOCDIR
# @DESCRIPTION:
# Sets the location of the doc builder config file.
#
# For sphinx this is the location of "conf.py"
# For mkdocs this is the location of "mkdocs.yml"
#
# Note that mkdocs.yml often does not reside
# in the same directory as the actual doc files
#
# Defaults to ${S}

# @ECLASS-VARIABLE: DOCDEPEND
# @DEFAULT_UNSET
# @PRE_INHERIT
# @DESCRIPTION:
# Sets additional dependencies to build docs.
# For sphinx and mkdocs these dependencies should
# be specified without [${PYTHON_USEDEP}], this
# is added by the eclass. E.g. to depend on mkdocs-material:
#
# DOCDEPEND="dev-python/mkdocs-material"
#
# This eclass appends to this variable, so you can
# call it later in your ebuild again if necessary.

# @ECLASS-VARIABLE: AUTODOC
# @PRE_INHERIT
# @DESCRIPTION:
# Sets whether to use sphinx.ext.autodoc/mkautodoc
# Defaults to 1 (True) for sphinx, and 0 (False) for mkdocs

# @ECLASS-VARIABLE: OUTDIR
# @DESCRIPTION:
# Sets where the compiled files will be put.
# There's no real reason to change this, but this
# variable is useful if you want to overwrite the HTML_DOCS
# added by this eclass. E.g.:
#
# HTML_DOCS=( "${yourdocs}" "${OUTDIR}/." )
#
# Defaults to ${DOCDIR}/_build/html

# @ECLASS-VARIABLE: DOCS_CONFIG_NAME
# @DESCRIPTION:
# Name of the doc builder config file.
#
# Only relevant for doxygen, as it allows
# config files with non-standard names
#
# Defaults to Doxyfile for doxygen

if [[ ! ${_DOCS} ]]; then

# For the python based DOCBUILDERS we need to inherit python-any-r1
case "${DOCBUILDER}" in
        "sphinx"|"mkdocs")
                # If this is not a python package then
                # this is not already set, so we need
                # to set this to inherit python-any-r1
                if [[ -z "${PYTHON_COMPAT}" ]]; then
                        PYTHON_COMPAT=( python3_{6,7,8} )
                fi

                # Inherit python-any-r1 if neither python-any-r1 nor
                # python-r1 have been inherited, because we need the
                # python_gen_any_dep function
                if [[ ! ${_PYTHON_R1} && ! ${_PYTHON_ANY_R1} ]]; then
                        inherit python-any-r1
                fi
                ;;
        "doxygen")
                # do not need to inherit anything for doxygen
                true
                ;;
        "")
                die "DOCBUILDER unset, should be set to use ${ECLASS}"
                ;;
        *)
                die "Unsupported DOCBUILDER=${DOCBUILDER} (unknown) for 
${ECLASS}"
                ;;
esac

# @FUNCTION: python_check_deps
# @DESCRIPTION:
# Check if the dependencies are valid
python_check_deps() {
        debug-print-function ${FUNCNAME}
        use doc || return 0

        local dep
        for dep in ${check_deps[@]}; do
                has_version "${dep}[${PYTHON_USEDEP}]" || return 1
        done
}
# Save this before we start manipulating it
check_deps=${DOCDEPEND}

# @FUNCTION: python_append_dep
# @DESCRIPTION:
# Appends [\${PYTHON_USEDEP}] to all dependencies
# for python based DOCBUILDERs such as mkdocs or
# sphinx.
python_append_deps() {
        debug-print-function ${FUNCNAME}

        local temp=()
        local dep
        for dep in ${DOCDEPEND[@]}; do
                temp+=" ${dep}[\${PYTHON_USEDEP}]"
        done
        DOCDEPEND=${temp}
}

# @FUNCTION: sphinx_setup
# @DESCRIPTION:
# Sets dependencies for sphinx
sphinx_setup() {
        debug-print-function ${FUNCNAME}

        : ${AUTODOC:=1}

        if [[ ${AUTODOC} == 0 && -n "${DOCDEPEND}" ]]; then
                die "${FUNCNAME}: do not set autodoc to 0 if external plugins 
are used"
        fi
        if [[ ${AUTODOC} == 1 ]]; then
                DOCDEPEND="$(python_gen_any_dep "
                        dev-python/sphinx[\${PYTHON_USEDEP}]
                        ${DOCDEPEND}")"

        else
                DOCDEPEND="dev-python/sphinx"
        fi
}

# @FUNCTION: sphinx_compile
# @DESCRIPTION:
# Calls sphinx to build docs.
#
# If you overwrite src_compile or python_compile_all
# do not call this function, call docs_compile instead
sphinx_compile() {
        debug-print-function ${FUNCNAME}
        use doc || return

        local confpy=${DOCDIR}/conf.py
        [[ -f ${confpy} ]] ||
                die "${confpy} not found, DOCDIR=${DOCDIR} call wrong"

        if [[ ${AUTODOC} == 0 ]]; then
                if grep -F -q 'sphinx.ext.autodoc' "${confpy}"; then
                        die "${FUNCNAME}: autodoc disabled but 
sphinx.ext.autodoc found in ${confpy}"
                fi
        elif [[ ${AUTODOC} == 1 ]]; then
                if ! grep -F -q 'sphinx.ext.autodoc' "${confpy}"; then
                        die "${FUNCNAME}: sphinx.ext.autodoc not found in 
${confpy}, set AUTODOC=0"
                fi
        fi
        
        sed -i -e 's:^intersphinx_mapping:disabled_&:' \
                "${DOCDIR}"/conf.py || die
        # not all packages include the Makefile in pypi tarball
        sphinx-build -b html -d "${DOCDIR}"/_build/doctrees "${DOCDIR}" \
        "${OUTDIR}" || die
}

# @FUNCTION: mkdocs_setup
# @DESCRIPTION:
# Sets dependencies for mkdocs
mkdocs_setup() {
        debug-print-function ${FUNCNAME}

        : ${AUTODOC:=0}

        if [[ ${AUTODOC} == 1 ]]; then
                DOCDEPEND="$(python_gen_any_dep "
                        dev-python/mkdocs[\${PYTHON_USEDEP}]
                        dev-python/mkautodoc[\${PYTHON_USEDEP}]
                ${DOCDEPEND}")"
        else
                DOCDEPEND="$(python_gen_any_dep "
                        dev-python/mkdocs[\${PYTHON_USEDEP}]
                        ${DOCDEPEND}")"
        fi
}

# @FUNCTION: mkdocs_compile
# @DESCRIPTION:
# Calls mkdocs to build docs.
#
# If you overwrite src_compile or python_compile_all
# do not call this function, call docs_compile instead
mkdocs_compile() {
        debug-print-function ${FUNCNAME}
        use doc || return

        local mkdocsyml=${DOCDIR}/mkdocs.yml
        [[ -f ${mkdocsyml} ]] ||
                die "${mkdocsyml} not found, DOCDIR=${DOCDIR} wrong"

        pushd "${DOCDIR}"
        mkdocs build -d "${OUTDIR}" || die
        popd

        # remove generated .gz variants
        # mkdocs currently has no option to disable this
        # and portage complains: "Colliding files found by ecompress"
        rm "${OUTDIR}"/*.gz || die
}

# @FUNCTION: doxygen_setup
# @DESCRIPTION:
# Sets dependencies for doxygen
doxygen_setup() {
        debug-print-function ${FUNCNAME}

        DOCDEPEND="app-doc/doxygen
                        ${DOCDEPEND}"
}

# @FUNCTION: doxygen_compile
# @DESCRIPTION:
# Calls doxygen to build docs.
#
# If you overwrite src_compile or python_compile_all
# do not call this function, call docs_compile instead
doxygen_compile() {
        debug-print-function ${FUNCNAME}
        use doc || return

        : ${DOCS_CONFIG_NAME:="Doxyfile"}

        local doxyfile=${DOCDIR}/${DOCS_CONFIG_NAME}
        [[ -f ${doxyfile} ]] ||
                die "${doxyfile} not found, DOCDIR=${DOCDIR} or 
DOCS_CONFIG_NAME=${DOCS_CONFIG_NAME} wrong"

        # doxygen wants the HTML_OUTPUT dir to already exist
        mkdir -p "${OUTDIR}"

        pushd "${DOCDIR}"
        (cat "${doxyfile}" ; echo "HTML_OUTPUT=${OUTDIR}") | doxygen - || die
        popd
}

# @FUNCTION: docs_compile
# @DESCRIPTION:
# Calls DOCBUILDER and sets HTML_DOCS
#
# This function must be called in global scope.  Take care not to
# overwrite the variables set by it. Has support for distutils-r1
# eclass, but only if this eclass is inherited *after*
# distutils-r1. If you need to extend src_compile() or
# python_compile_all(), you can call the original implementation
# as docs_compile.
docs_compile() {
        debug-print-function ${FUNCNAME}
        use doc || return

        # Set a sensible default as DOCDIR
        : ${DOCDIR:="${S}"}

        # Where to put the compiled files?
        : ${OUTDIR:="${DOCDIR}/_build/html"}

        case "${DOCBUILDER}" in
                "sphinx")
                        sphinx_compile
                        ;;
                "mkdocs")
                        mkdocs_compile
                        ;;
                "doxygen")
                        doxygen_compile
                        ;;
        esac

        HTML_DOCS+=( "${OUTDIR}/." )

        # we need to ensure successful return in case we're called last,
        # otherwise Portage may wrongly assume sourcing failed
        return 0
}


# This is where we setup the USE/(B)DEPEND variables
# and call the doc builder specific setup functions
IUSE+=" doc"

# Call the correct setup function
case "${DOCBUILDER}" in
        "sphinx")
                python_append_deps
                sphinx_setup
                ;;
        "mkdocs")
                python_append_deps
                mkdocs_setup
                ;;
        "doxygen")
                doxygen_setup
                ;;
esac

if [[ ${EAPI} == [56] ]]; then
        DEPEND+=" doc? ( ${DOCDEPEND} )"
else
        BDEPEND+=" doc? ( ${DOCDEPEND} )"
fi

# If this is a python package using distutils-r1
# then put the compile function in the specific
# python function, else just put it in src_compile
if [[ ${_DISTUTILS_R1} && ( ${DOCBUILDER}="mkdocs" || ${DOCBUILDER}="sphinx" ) 
]]; then
        python_compile_all() { docs_compile; }
else
        src_compile() { docs_compile; }
fi

_DOCS=1
fi

Reply via email to