# Copyright 2023-2025 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 # @ECLASS: pypi.eclass # @MAINTAINER: # Michał Górny # @AUTHOR: # Michał Górny # @SUPPORTED_EAPIS: 7 8 # @BLURB: A helper eclass to generate PyPI source URIs # @DESCRIPTION: # The pypi.eclass can be used to easily obtain URLs for artifacts # uploaded to PyPI.org. When inherited, the eclass defaults SRC_URI # and S to fetch .tar.gz sdist. The eclass defaults to using ${PN} # as PyPI project name; PYPI_PN can be set prior to inheriting it # to override that. The source distribution filename is assumed # to be normalized by default; PYPI_NO_NORMALIZE can be set prior # to inherit to override that. The version is translated using # pypi_translate_version. # # If PYPI_VERIFY_REPO is set to a non-empty value, verify-provenance # flag is added along with necessary BDEPEND. The provenance file # for the default source distribution is added to SRC_URI, and a default # src_unpack() is exported to verify its provenance. # # If necessary, SRC_URI and S can be overridden by the ebuild. Three # helper functions, pypi_sdist_url, pypi_wheel_url # and pypi_provenance_url are provided to generate URLs to artifacts # of specified type, with customizable URL components. Additionally, # pypi_wheel_name can be used to generate the wheel filename. # # pypi_normalize_name can be used to normalize an arbitrary project name # according to sdist/wheel normalization rules. pypi_translate_version # can be used to translate a Gentoo version string into its PEP 440 # equivalent. # # pypi_verify_provenance can be used to verify the provenance directly. # # @EXAMPLE: # @CODE # inherit pypi # # SRC_URI="$(pypi_sdist_url "${PN^}" "${PV}")" # S=${WORKDIR}/${P^} # @CODE case ${EAPI} in 7|8) ;; *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;; esac if [[ -z ${_PYPI_ECLASS} ]]; then _PYPI_ECLASS=1 # @ECLASS_VARIABLE: PYPI_NO_NORMALIZE # @PRE_INHERIT # @DEFAULT_UNSET # @DESCRIPTION: # When set to a non-empty value, disables project name normalization # for the default SRC_URI and S values. # @ECLASS_VARIABLE: PYPI_PN # @PRE_INHERIT # @DESCRIPTION: # The PyPI project name. This should be overridden scarcely, generally # when upstream project name does not conform to Gentoo naming rules, # e.g. when it contains dots or uppercase letters. # # Example use: # @CODE # PYPI_PN=${PN/-/.} # @CODE : "${PYPI_PN:=${PN}}" # @ECLASS_VARIABLE: PYPI_VERIFY_REPO # @DEFAULT_UNSET # @PRE_INHERIT # @DESCRIPTION: # The repository to verify provenance against. If set to a non-empty # value, the eclass will add a "verify-provenance" flag that can be used # to download the provenance for the distribution, and verify it # against the provenance and the specified repository. # @FUNCTION: _pypi_normalize_name # @INTERNAL # @USAGE: # @DESCRIPTION: # Internal normalization function, returns the result # via _PYPI_NORMALIZED_NAME variable. _pypi_normalize_name() { # NB: it's fine to alter it unconditionally since this function is # always called from a subshell or in global scope # (via _pypi_set_globals) shopt -s extglob local name=${1//+([._-])/_} shopt -u extglob _PYPI_NORMALIZED_NAME="${name,,}" } # @FUNCTION: pypi_normalize_name # @USAGE: # @DESCRIPTION: # Normalize the project name according to sdist/wheel normalization # rules. That is, convert to lowercase and replace runs of [._-] # with a single underscore. # # Based on the spec, as of 2023-02-10: # https://packaging.python.org/en/latest/specifications/#package-distribution-file-formats pypi_normalize_name() { [[ ${#} -ne 1 ]] && die "Usage: ${FUNCNAME} " local _PYPI_NORMALIZED_NAME _pypi_normalize_name "${@}" echo "${_PYPI_NORMALIZED_NAME}" } # @FUNCTION: _pypi_translate_version # @USAGE: # @DESCRIPTION: # Internal version translation function, returns the result # via _PYPI_TRANSLATED_VERSION variable. _pypi_translate_version() { local version=${1} version=${version/_alpha/a} version=${version/_beta/b} version=${version/_pre/.dev} version=${version/_rc/rc} _PYPI_TRANSLATED_VERSION=${version/_p/.post} } # @FUNCTION: pypi_translate_version # @USAGE: # @DESCRIPTION: # Translate the specified Gentoo version into the usual Python # counterpart. Assumes PEP 440 versions. # # Note that we do not have clear counterparts for the epoch segment, # nor for development release segment. pypi_translate_version() { [[ ${#} -ne 1 ]] && die "Usage: ${FUNCNAME} " local _PYPI_TRANSLATED_VERSION _pypi_translate_version "${@}" echo "${_PYPI_TRANSLATED_VERSION}" } # @FUNCTION: _pypi_sdist_url # @INTERNAL # @USAGE: [--no-normalize] [ [ []]] # @DESCRIPTION: # Internal sdist generated, returns the result via _PYPI_SDIST_URL # variable. _pypi_sdist_url() { local normalize=1 if [[ ${1} == --no-normalize ]]; then normalize= shift fi if [[ ${#} -gt 3 ]]; then die "Usage: ${FUNCNAME} [--no-normalize] [ []]" fi local project=${1-"${PYPI_PN}"} local version=${2-"$(pypi_translate_version "${PV}")"} local suffix=${3-.tar.gz} local _PYPI_NORMALIZED_NAME=${project} [[ ${normalize} ]] && _pypi_normalize_name "${_PYPI_NORMALIZED_NAME}" _PYPI_SDIST_URL="https://files.pythonhosted.org/packages/source/${project::1}/${project}/${_PYPI_NORMALIZED_NAME}-${version}${suffix}" } # @FUNCTION: pypi_sdist_url # @USAGE: [--no-normalize] [ [ []]] # @DESCRIPTION: # Output the URL to PyPI sdist for specified project/version tuple. # # The `--no-normalize` option disables project name normalization # for sdist filename. This may be necessary when dealing with distfiles # generated using build systems that did not follow PEP 625 # (i.e. the sdist name contains uppercase letters, hyphens or dots). # # If is unspecified, it defaults to ${PYPI_PN}. The package # name is normalized according to the specification unless # `--no-normalize` is passed. # # If is unspecified, it defaults to ${PV} translated # via pypi_translate_version. If it is specified, then it is used # verbatim (the function can be called explicitly to translate custom # version number). # # If is unspecified, it defaults to ".tar.gz". Another valid # value is ".zip" (please remember to add a BDEPEND on app-arch/unzip). pypi_sdist_url() { local _PYPI_SDIST_URL _pypi_sdist_url "${@}" echo "${_PYPI_SDIST_URL}" } # @FUNCTION: pypi_wheel_name # @USAGE: [ [ [ []]]] # @DESCRIPTION: # Output the wheel filename for the specified project/version tuple. # # If is unspecified, it defaults to ${PYPI_PN}. The package # name is normalized according to the wheel specification. # # If is unspecified, it defaults to ${PV} translated # via pypi_translate_version. If it is specified, then it is used # verbatim (the function can be called explicitly to translate custom # version number). # # If is unspecified, it defaults to "py3". It can also be # "py2.py3", or a specific version in case of non-pure wheels. # # If is unspecified, it defaults to "none-any". # You need to specify the correct value for non-pure wheels, # e.g. "abi3-linux_x86_64". pypi_wheel_name() { if [[ ${#} -gt 4 ]]; then die "Usage: ${FUNCNAME} [ [ []]]" fi local _PYPI_NORMALIZED_NAME _pypi_normalize_name "${1:-"${PYPI_PN}"}" local version=${2-"$(pypi_translate_version "${PV}")"} local pytag=${3-py3} local abitag=${4-none-any} echo "${_PYPI_NORMALIZED_NAME}-${version}-${pytag}-${abitag}.whl" } # @FUNCTION: pypi_wheel_url # @USAGE: [--unpack] [ [ [ []]]] # @DESCRIPTION: # Output the URL to PyPI wheel for specified project/version tuple. # # The `--unpack` option causes a SRC_URI with an arrow operator to # be generated, that adds a .zip suffix to the fetched distfile, # so that it is unpacked in default src_unpack(). Note that # the wheel contents will be unpacked straight into ${WORKDIR}. # You need to add a BDEPEND on app-arch/unzip. # # If is unspecified, it defaults to ${PYPI_PN}. # # If is unspecified, it defaults to ${PV} translated # via pypi_translate_version. If it is specified, then it is used # verbatim (the function can be called explicitly to translate custom # version number). # # If is unspecified, it defaults to "py3". It can also be # "py2.py3", or a specific version in case of non-pure wheels. # # If is unspecified, it defaults to "none-any". # You need to specify the correct value for non-pure wheels, # e.g. "abi3-linux_x86_64". pypi_wheel_url() { local unpack= if [[ ${1} == --unpack ]]; then unpack=1 shift fi if [[ ${#} -gt 4 ]]; then die "Usage: ${FUNCNAME} [--unpack] [ [ []]]" fi local filename=$(pypi_wheel_name "${@}") local project=${1-"${PYPI_PN}"} local version=${2-"$(pypi_translate_version "${PV}")"} local pytag=${3-py3} printf "https://files.pythonhosted.org/packages/%s" \ "${pytag}/${project::1}/${project}/${filename}" if [[ ${unpack} ]]; then echo " -> ${filename}.zip" fi } # @FUNCTION: _pypi_provenance_url # @INTERNAL # @USAGE: [ []] # @DESCRIPTION: # Internal function to generate provenance URL for , using # specified and . Returns the result # via _PYPI_ATTESTATION_URL. _pypi_provenance_url() { if [[ ${#} -lt 1 || ${#} -gt 3 ]]; then die "Usage: ${FUNCNAME} [ []]" fi local dist=${1} local project=${2-"${PYPI_PN}"} local version=${3-"$(pypi_translate_version "${PV}")"} _PYPI_ATTESTATION_URL="https://pypi.org/integrity/${project}/v${version}/${dist}/provenance" } # @FUNCTION: pypi_provenance_url # @USAGE: [ []] # @DESCRIPTION: # Output the URL to PyPI provenance for the specified artifact. pypi_provenance_url() { local _PYPI_PROVENANCE_URL _pypi_provenance_url "${@}" echo "${_PYPI_PROVENANCE_URL}" } # @FUNCTION: pypi_verify_provenance # @USAGE: [] # @DESCRIPTION: # Verify the specified artifact's provenance. is the path # to the artifact to verify, while is the provenance file. # # The function defaults to using PYPI_VERIFY_REPO as the expected # repository URL. This can be overridden by specfying . # # The function dies on verification failure. pypi_verify_provenance() { if [[ ${#} -lt 2 || ${#} -gt 3 ]]; then die "Usage: ${FUNCNAME} []" fi local dist=${1} local provenance=${2} local repo=${3-"${PYPI_VERIFY_REPO}"} einfo "Verifying ${dist##*/} ..." pypi-attestations verify pypi --offline \ --repository "${repo}" \ --provenance-file "${provenance}" \ "${dist}" || die "Provenance verification failed for ${dist##*/}" } # @FUNCTION: pypi_src_unpack # @DESCRIPTION: # A src_unpack implementation that verifies provenances. Exported only # with PYPI_VERIFY_REPO. pypi_src_unpack() { if use verify-provenance; then local sdist_url=$(pypi_sdist_url) local filename=${sdist_url##*/} pypi_verify_provenance "${DISTDIR}/${filename}"{,.provenance} fi default } # @FUNCTION: _pypi_set_globals # @INTERNAL # @DESCRIPTION: # Set global variables, SRC_URI and S. _pypi_set_globals() { local _PYPI_SDIST_URL _PYPI_TRANSLATED_VERSION _pypi_translate_version "${PV}" if [[ ${PYPI_NO_NORMALIZE} ]]; then _pypi_sdist_url --no-normalize "${PYPI_PN}" "${_PYPI_TRANSLATED_VERSION}" S="${WORKDIR}/${PYPI_PN}-${_PYPI_TRANSLATED_VERSION}" else local _PYPI_NORMALIZED_NAME _pypi_normalize_name "${PYPI_PN}" _pypi_sdist_url "${PYPI_PN}" "${_PYPI_TRANSLATED_VERSION}" S="${WORKDIR}/${_PYPI_NORMALIZED_NAME}-${_PYPI_TRANSLATED_VERSION}" fi SRC_URI=${_PYPI_SDIST_URL} if [[ -n ${PYPI_VERIFY_REPO} ]]; then local dist=${_PYPI_SDIST_URL##*/} local _PYPI_ATTESTATION_URL _pypi_provenance_url "${dist}" "${PYPI_PN}" \ "${_PYPI_TRANSLATED_VERSION}" IUSE="verify-provenance" SRC_URI+=" verify-provenance? ( ${_PYPI_ATTESTATION_URL} -> ${dist}.provenance ) " BDEPEND=" verify-provenance? ( dev-python/pypi-attestations ) " fi } _pypi_set_globals if [[ -n ${PYPI_VERIFY_REPO} ]]; then EXPORT_FUNCTIONS src_unpack fi fi