From: Matt Jolly <kan...@gentoo.org> The rust eclass acts similarly to the llvm eclass.
It works with optional `RUST_{MAX,MIN}_SLOT` variables to enable ebuilds to trivially generate (and enforce) dependencies on an appropriate Rust SLOT. A `RUST_NEEDS_LLVM` variable can be set to have the eclass read `LLVM_COMPAT` and generate an llvm-r1-USE-gated dependency string for use in the ebuild, storing the result in `RUST_LLVM_DEP` for consumption. `llvm_gen_dep` is not suitable; see the eclass for detail on why a `rust_llvm_gen_dep` was not implemented. The default `rust_pkg_setup` will prefix the selected slot to `PATH` and export `RUSTC` and `CARGO` variables pointing to that slot for ease-of-use. This should prevent issues like: Bug: https://bugs.gentoo.org/907492 Bug: https://bugs.gentoo.org/942444 Signed-off-by: Matt Jolly <kan...@gentoo.org> --- eclass/rust.eclass | 480 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 480 insertions(+) create mode 100644 eclass/rust.eclass diff --git a/eclass/rust.eclass b/eclass/rust.eclass new file mode 100644 index 000000000000..bf9c47cd7f4b --- /dev/null +++ b/eclass/rust.eclass @@ -0,0 +1,480 @@ +# Copyright 1999-2024 Gentoo Authors +# Distributed under the terms of the GNU General Public License v2 + +# @ECLASS: rust.eclass +# @MAINTAINER: +# Matt Jolly <kan...@gentoo.org> +# @AUTHOR: +# Matt Jolly <kan...@gentoo.org> +# @SUPPORTED_EAPIS: 8 +# @BLURB: Utility functions to build against slotted Rust +# @DESCRIPTION: +# An eclass to reliably depend on a Rust or Rust/LLVM combination for +# a given Rust slot. To use the eclass: +# +# 1. If required, set RUST_{MAX,MIN}_SLOT to the range of supported slots. +# 2. Use rust_gen_deps to add appropriate dependencies. (rust_gen_llvm_deps for LLVM) +# 3. Use rust_pkg_setup, get_rust_prefix or RUST_SLOT. + +# Example use for a package supporting Rust 1.72.0 to 1.82.0: +# @CODE +# +# RUST_MAX_VER="1.82.0" +# RUST_MIN_VER="1.72.0" +# +# inherit meson rust +# +# BDEPEND=" +# $(rust_gen_deps) +# " +# +# # only if you need to define one explicitly +# pkg_setup() { +# rust_pkg_setup +# do-something-else +# } +# @CODE +# +# Example for a package needing Rust w/ a specific target: +# @CODE +# inherit meson rust +# +# RDEPEND=" +# $(rust_gen_deps) +# " +# DEPEND=${RDEPEND} +# +# rust_check_deps() { +# local rust_slot rust_type +# { read -r RUST_SLOT; read -r RUST_TYPE; } <<< $(rust_check_usedep ${RUST_SLOT} "clippy,${MULTILIB_USEDEP}") +# if [[ -n ${RUST_SLOT} ]] && [[ -n ${RUST_TYPE} ]]; then +# return 0 +# else +# return 1 +# fi +# } +# @CODE + +case ${EAPI} in + 8) ;; + *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;; +esac + +if [[ -z ${_RUST_ECLASS} ]]; then +_RUST_ECLASS=1 + +# == internal control knobs == + +# @ECLASS_VARIABLE: _RUST_KNOWN_SLOTS +# @INTERNAL +# @DESCRIPTION: +# Definitive list of Rust slots and the associated LLVM slot, newest first. +declare -A -g -r _RUST_KNOWN_SLOTS=( + ["1.82.0"]=19 + ["1.81.0"]=18 + ["1.80.1"]=18 + ["1.79.0"]=18 + ["1.77.1"]=17 + ["1.75.0"]=17 + ["1.74.1"]=17 + ["1.71.1"]=16 +) + +# @ECLASS_VARIABLE: _RUST_SLOTS_ORDERED +# @INTERNAL +# @DESCRIPTION: +# Array of Rust slots, newest first. +# While _RUST_KNOWN_SLOTS stores useful info about the relationship between Rust and LLVM slots, +# this array is used to store the Rust slots in a more convenient order for iteration. +declare -a -g -r _RUST_SLOTS_ORDERED=( + "1.82.0" + "1.81.0" + "1.80.1" + "1.79.0" + "1.77.1" + "1.75.0" + "1.74.1" + "1.71.1" +) + +# == control variables == + +# @ECLASS_VARIABLE: RUST_MAX_VER +# @DEFAULT_UNSET +# @DESCRIPTION: +# Highest Rust slot supported by the package. Needs to be set before +# rust_pkg_setup is called. If unset, no upper bound is assumed. + +# @ECLASS_VARIABLE: RUST_MIN_VER +# @DEFAULT_UNSET +# @DESCRIPTION: +# Lowest Rust slot supported by the package. Needs to be set before +# rust_pkg_setup is called. If unset, no lower bound is assumed. + +# @eclass-variable: RUST_NEEDS_LLVM +# @DEFAULT_UNSET +# @DESCRIPTION: +# If set to a non-empty value generate a llvm_slot_${llvm_slot}? gated +# dependency block for rust slots in LLVM_COMPAT. This is useful for +# packages that need a tight coupling between Rust and LLVM but don't +# really care _which_ version of Rust is selected. Combine with +# RUST_MAX_VER and RUST_MIN_VER to limit the range of Rust versions +# that are acceptable. Will `die` if llvm-r1 is not inherited or +# an invalid combination of RUST and LLVM slots is detected; this probably +# means that a LLVM slot in LLVM_COMPAT has had all of its Rust slots filtered. + +# @ECLASS_VARIABLE: RUST_LLVM_DEP +# @OUTPUT_VARIABLE +# @DESCRIPTION: +# This is an eclass-generated, llvm-r1 USE gated, Rust dependency string +# for all LLVM implementations listed in LLVM_COMPAT, filtered by +# RUST_MAX_VER and RUST_MIN_VER. + +# == global metadata == + +_rust_set_globals() { + debug-print-function ${FUNCNAME} "$@" + + if [[ -n ${RUST_MAX_VER} && -n ${RUST_MIN_VER} ]]; then + if ! ver_test ${RUST_MAX_VER} -ge ${RUST_MIN_VER}; then + die "RUST_MAX_VER must be greater than or equal to RUST_MIN_VER" + fi + fi + + # Make an array of slots that are acceptable + local acceptable_slots=() + local slot + # Try to keep this in order of newest to oldest + for slot in "${_RUST_SLOTS_ORDERED[@]}"; do + if [[ -z "${RUST_MAX_VER}" ]] || ver_test ${slot} -le ${RUST_MAX_VER}; then + if [[ -z "${RUST_MIN_VER}" ]] || ver_test ${slot} -ge ${RUST_MIN_VER}; then + acceptable_slots+=( "${slot}" ) + fi + fi + done + + _RUST_SLOTS=( "${acceptable_slots[@]}" ) + readonly _RUST_SLOTS + + # The alternative to this monstrosity is to consume _LLVM_SLOTS from llvm-r1.eclass + # and have our own `rust_gen_llvm_dep` to programmatically generate the deps. + # To be good Gentoo netizens we're not going to consume internal variables from another eclass + # without first asking the maintainer of that eclass if it's okay; this should be "good enough". + if [[ -n "${RUST_NEEDS_LLVM}" ]]; then + # This is a proxy for LLVM_COMPAT, but it doesn't make sense to use the generated + # RUST_LLVM_DEPS without llvm-r1 so we're probably fine. + if [[ -z ${_LLVM_R1_ECLASS} ]]; then + die "${FUNCNAME}: llvm-r1.eclass is required" + fi + + local llvm_dep=() + local llvm_slot + local rust_slot + for llvm_slot in ${LLVM_COMPAT[@]}; do + # Quick sanity check to make sure that the llvm slot is valid for Rust. + if [[ "${_RUST_KNOWN_SLOTS[@]}" == *"${llvm_slot}"* ]]; then + # We're working a bit backwards here; iterate over RUST_KNOWN_SLOTS, check the + # LLVM slot, and if it matches add this to a new array because it may (and likely will) + # match multiple Rust slots. + # We already filtered Rust slots that were too old or new for this ebuild! + local slot_dep_content=() + for rust_slot in "${_RUST_SLOTS[@]}"; do + if [[ ${_RUST_KNOWN_SLOTS[${rust_slot}]} == ${llvm_slot} ]]; then + slot_dep_content+=( "dev-lang/rust:${rust_slot}[llvm_slot_${llvm_slot}]" ) + slot_dep_content+=( "dev-lang/rust-bin:${rust_slot}[llvm_slot_${llvm_slot}]" ) + fi + + done + if [ ${#slot_dep_content[@]} -ne 0 ]; then + llvm_dep+=( "llvm_slot_${llvm_slot}? ( || ( ${slot_dep_content[*]} ) )" ) + else + die "${FUNCNAME}: no Rust slots found for LLVM slot ${llvm_slot}" + fi + fi + done + RUST_LLVM_DEPS="${llvm_dep[*]}" + readonly RUST_LLVM_DEPS + fi +} +_rust_set_globals +unset -f _rust_set_globals + +# == metadata helpers == + +# @FUNCTION: rust_gen_dep +# @USAGE: [usedep] +# @DESCRIPTION: +# Output a dependency block that will match any suitable Rust SLOT. +# The dependency will match either sys-devel/rust:${RUST_SLOT} +# or sys-devel/rust-bin:${RUST_SLOT}. +# +# Example: +# @CODE +# DEPEND=" +# $(rust_gen_dep "${USEDEP}") +# @CODE +rust_gen_dep() { + debug-print-function ${FUNCNAME} "$@" + + if [[ ${#} -gt 1 ]]; then + die "Usage: ${FUNCNAME} [usedep]" + fi + + local usedep + + if [[ ${#} -eq 1 ]]; then + usedep=${1} + fi + + local slot + + local RUST_DEPS=() + RUST_DEPS+=( "|| (" ) + for slot in "${_RUST_SLOTS[@]}"; do + if [[ -n ${usedep} ]]; then + RUST_DEPS+=( "dev-lang/rust:${slot}[${usedep}] dev-lang/rust-bin:${slot}[${usedep}]" ) + else + RUST_DEPS+=( "dev-lang/rust:${slot} dev-lang/rust-bin:${slot}" ) + fi + done + RUST_DEPS+=( ")" ) + echo "${RUST_DEPS[@]}" +} + +# == ebuild helpers == + +# @FUNCTION: rust_check_usedep +# @USAGE: [-b|-d] slot usedep +# @DESCRIPTION: +# Check if a Rust package with the specified SLOT and USE-dependency +# is installed. If -b is specified, the checks are performed relative +# to BROOT. If -d is specified, the checks are performed relative to +# ESYSROOT. -d is the default. +# +# Returns SLOT and Rust type if the slot is suitable. +# Rust type is either "source" or "binary". +rust_check_usedep() +{ + local hv_switch=-d + while [[ ${1} == -* ]]; do + case ${1} in + -b|-d) hv_switch=${1};; + *) break;; + esac + shift + done + + local slot=${1} + local usedep=${2} + + if ( has_version ${hv_switch} "dev-lang/rust:${slot}[${usedep}]" || + has_version ${hv_switch} "dev-lang/rust-bin:${slot}[${usedep}]" ); then + if has_version ${hv_switch} "dev-lang/rust:${slot}[${usedep}]"; then + rust_type="source" + else + rust_type="binary" + fi + echo ${slot} + echo ${rust_type} + fi +} + +# @FUNCTION: get_rust_slot +# @USAGE: [-b|-d] +# @DESCRIPTION: +# Find the newest Rust install that is acceptable for the package, +# and print its version number (i.e. SLOT) and type (source or bin[ary]). +# +# If -b is specified, the checks are performed relative to BROOT, +# and BROOT-path is returned. +# +# If -d is specified, the checks are performed relative to ESYSROOT, +# and ESYSROOT-path is returned. -d is the default. +# +# If RUST_M{AX,IN}_SLOT is non-zero, then only Rust versions that +# are not newer or older than the specified slot(s) will be considered. +# Otherwise, all Rust versions are be considered acceptable. +# +# If the `rust_check_deps()` function is defined within the ebuild, it +# will be called to verify whether a particular slot is accepable. +# Within the function scope, RUST_SLOT and LLVM_SLOT will be defined. +# +# The function should return a true status if the slot is acceptable, +# false otherwise. If rust_check_deps() is not defined, the function +# defaults to checking whether a suitable Rust package is installed. +get_rust_slot() { + debug-print-function ${FUNCNAME} "$@" + + local hv_switch=-d + while [[ ${1} == -* ]]; do + case ${1} in + -b|-d) hv_switch=${1};; + *) break;; + esac + shift + done + + local max_slot + if [[ -z ${RUST_MAX_VER} ]]; then + max_slot= + else + max_slot=${RUST_MAX_VER} + fi + local slot + local llvm_slot + + if [[ -n ${RUST_NEEDS_LLVM} ]]; then + local llvm_r1_slot + # quickly get a list of unique llvm slots that we support + for llvm_slot in $(echo "${_RUST_KNOWN_SLOTS[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '); do + if [[ "${LLVM_COMPAT[@]}" == *"${llvm_slot}"* ]]; then + # We can check for the USE + use llvm_slot_${llvm_slot} && llvm_r1_slot=${llvm_slot} + else + continue + fi + done + if [[ -z ${llvm_r1_slot} ]]; then + die "${FUNCNAME}: no LLVM slot found" + fi + fi + + # iterate over known slots, newest first + for slot in "${_RUST_SLOTS_ORDERED[@]}"; do + llvm_slot=${_RUST_KNOWN_SLOTS[${slot}]} + # skip higher slots + if [[ -n ${max_slot} ]]; then + if ver_test ${slot} -eq ${max_slot}; then + max_slot= + elif ver_test ${slot} -gt ${max_slot}; then + continue + fi + fi + + # If we're in LLVM mode we can skip any slots that don't match the selected USE + if [[ -n ${RUST_NEEDS_LLVM} ]]; then + if [[ ${llvm_slot} != ${llvm_r1_slot} ]]; then + continue + fi + fi + + if declare -f rust_check_deps >/dev/null; then + local RUST_SLOT=${slot} + local LLVM_SLOT=${_RUST_KNOWN_SLOTS[${slot}]} + rust_check_deps && return + else + local rust_type + # Check for an appropriate Rust version and its type. + # Prefer the from-source version "because" + if (has_version ${hv_switch} "dev-lang/rust:${slot}" || + has_version ${hv_switch} "dev-lang/rust-bin:${slot}"); then + if has_version ${hv_switch} "dev-lang/rust:${slot}"; then + rust_type="source" + else + rust_type="binary" + fi + echo ${slot} + echo ${rust_type} + return + fi + fi + + # We want to process the slot before escaping the loop if we've hit the minimum slot + if [[ -n ${RUST_MIN_VER} ]]; then + if ver_test ${slot} -eq ${RUST_MIN_VER}; then + break + fi + fi + + done + + # max_slot should have been unset in the iteration + if [[ -n ${max_slot} ]]; then + die "${FUNCNAME}: invalid max_slot=${max_slot}" + fi + + die "No Rust slot${1:+ <= ${1}} satisfying the package's dependencies found installed!" +} + +# @FUNCTION: get_rust_prefix +# @USAGE: [-b|-d] +# @DESCRIPTION: +# Find the newest Rust install that is acceptable for the package, +# and print an absolute path to it. If both -bin and regular Rust +# are installed, the regular Rust is preferred. +# +# The options and behavior are the same as get_rust_slot. +get_rust_prefix() { + debug-print-function ${FUNCNAME} "$@" + + local prefix=${ESYSROOT} + [[ ${1} == -b ]] && prefix=${BROOT} + + local slot rust_type + { read -r slot; read -r rust_type; } <<< $(get_rust_slot) + + if [[ ${rust_type} == "source" ]]; then + echo "${prefix}/usr/lib/rust/${slot}/" + else + echo "${prefix}/opt/rust-bin-${slot}/" + fi +} + +# @FUNCTION: rust_prepend_path +# @USAGE: <slot> <type> +# @DESCRIPTION: +# Prepend the path to the specified Rust to PATH and re-export it. +rust_prepend_path() { + debug-print-function ${FUNCNAME} "$@" + + [[ ${#} -ne 2 ]] && die "Usage: ${FUNCNAME} <slot> <type>" + local slot=${1} + + local rust_path + if [[ ${2} == "source" ]]; then + rust_path=${ESYSROOT}/usr/lib/rust/${slot}/bin + else + rust_path=${ESYSROOT}/opt/rust-bin-${slot}/bin + fi + + export PATH="${rust_path}:${PATH}" +} + +# @FUNCTION: rust_pkg_setup +# @DESCRIPTION: +# Prepend the appropriate executable directory for the newest +# acceptable Rust slot to the PATH. If used with LLVM, an appropriate +# `llvm_pkg_setup` call should be made in addition to this function. +# For path determination logic, please see the get_rust_prefix documentation. +# +# The highest acceptable Rust slot can be set in RUST_MAX_VER variable. +# If it is unset or empty, any slot is acceptable. +# +# The lowest acceptable Rust slot can be set in RUST_MIN_VER variable. +# If it is unset or empty, any slot is acceptable. +# +# `CARGO` and `RUSTC` variables are set for the selected slot and exported. +# +# The PATH manipulation is only done for source builds. The function +# is a no-op when installing a binary package. +# +# If any other behavior is desired, the contents of the function +# should be inlined into the ebuild and modified as necessary. +rust_pkg_setup() { + debug-print-function ${FUNCNAME} "$@" + + if [[ ${MERGE_TYPE} != binary ]]; then + { read -r RUST_SLOT; read -r RUST_TYPE; } <<< $(get_rust_slot) + rust_prepend_path "${RUST_SLOT}" "${RUST_TYPE}" + CARGO="$(get_rust_prefix)bin/cargo" + RUSTC="$(get_rust_prefix)bin/rustc" + export CARGO RUSTC + einfo "Using Rust ${RUST_SLOT} (${RUST_TYPE})" + einfo "CARGO=${CARGO}" + einfo "RUSTC=${RUSTC}" + fi +} + +fi + +EXPORT_FUNCTIONS pkg_setup -- 2.47.0