diff --git a/Runner/config/pkg_command_map.conf b/Runner/config/pkg_command_map.conf new file mode 100755 index 00000000..82b941f5 --- /dev/null +++ b/Runner/config/pkg_command_map.conf @@ -0,0 +1,152 @@ +# qcom-linux-testkit command-to-package map +# +# Format: +# os-id:command=package package2 ... +# provider:command=package package2 ... +# os-id:TESTNAME:command=package package2 ... +# provider:TESTNAME:command=package package2 ... +# +# Lookup order: +# 1. os-id:TESTNAME:command +# 2. provider:TESTNAME:command +# 3. os-id:command +# 4. provider:command +# +# The right-hand side is a required package set. +# All packages listed on the same line are installed when that exact +# dependency command is missing. +# +# Use TESTNAME-scoped mappings for large feature stacks so only that test +# installs those packages. +# +# The provider installs package names through the active package manager. +# It does not scrape raw package pool directories. +# +# For apt: +# - Internal repositories should be provided through *.sources files. +# - Public repositories can use the target's existing apt sources. +# - Local package locations should be exposed as a local apt repository +# through a *.sources file under: +# +# /opt/qcom-testkit/metadata/*.sources +# +# Apt selects the latest candidate version from repository metadata. +# Do not encode versions here unless strict pinning is required. +# +# Do not assume command name equals package name. + +apt:modetest=libdrm-tests +apt:kmscube=kmscube +apt:glmark2=glmark2 +apt:v4l2-ctl=v4l-utils +apt:media-ctl=v4l-utils +apt:yavta=yavta +apt:jq=jq +apt:wget=wget +apt:curl=curl +apt:tar=tar +apt:unzip=unzip +apt:stress-ng=stress-ng +apt:iperf3=iperf3 +apt:bluetoothctl=bluez +apt:gst-launch-1.0=gstreamer1.0-tools +apt:gst-inspect-1.0=gstreamer1.0-tools +apt:efivar=efivar + +# --------------------------------------------------------------------------- +# Common rpm provider mappings. +# Applies to Yocto+dNF/RPM, Red Hat, CentOS, Fedora, etc. +# --------------------------------------------------------------------------- + +rpm:modetest=libdrm-tests +rpm:kmscube=kmscube +rpm:glmark2=glmark2 +rpm:v4l2-ctl=v4l-utils +rpm:media-ctl=v4l-utils +rpm:yavta=yavta +rpm:jq=jq +rpm:wget=wget +rpm:curl=curl +rpm:tar=tar +rpm:unzip=unzip +rpm:stress-ng=stress-ng +rpm:iperf3=iperf3 +rpm:bluetoothctl=bluez +rpm:gst-launch-1.0=gstreamer1.0-tools +rpm:gst-inspect-1.0=gstreamer1.0-tools +rpm:efivar=efivar + +# --------------------------------------------------------------------------- +# Common opkg provider mappings. +# Override per distro if package names differ. +# --------------------------------------------------------------------------- + +opkg:modetest=libdrm-tests +opkg:kmscube=kmscube +opkg:glmark2=glmark2 +opkg:v4l2-ctl=v4l-utils +opkg:media-ctl=v4l-utils +opkg:yavta=yavta +opkg:jq=jq +opkg:wget=wget +opkg:curl=curl +opkg:tar=tar +opkg:unzip=unzip +opkg:stress-ng=stress-ng +opkg:iperf3=iperf3 +opkg:bluetoothctl=bluez5 +opkg:gst-launch-1.0=gstreamer1.0 +opkg:gst-inspect-1.0=gstreamer1.0 +opkg:efivar=efivar + +# --------------------------------------------------------------------------- +# Qualcomm sensors test-specific package set. +# +# These mappings are TESTNAME-scoped, so sensor packages are installed only +# when the sensor test runs and check_dependencies() checks sns_test. +# +# Update TESTNAME/command if the actual sensor test uses a different +# TESTNAME or dependency binary. +# --------------------------------------------------------------------------- + +apt:Sensors:sns_test=qcom-sensors-api qcom-sensors-core qcom-sensors-registry qcom-sensors-services qcom-sensors-test-core qcom-sensors-test-apps +rpm:Sensors:sns_test=qcom-sensors-api qcom-sensors-core qcom-sensors-registry qcom-sensors-services qcom-sensors-test-core qcom-sensors-test-apps +opkg:Sensors:sns_test=qcom-sensors-api qcom-sensors-core qcom-sensors-registry qcom-sensors-services qcom-sensors-test-core qcom-sensors-test-apps + +# If the sensor test requires development/header packages, use the below +# TESTNAME-specific mapping instead of the smaller runtime-only mapping above. +# +# apt:Sensors_Validation:sns_test=qcom-sensors-api qcom-sensors-api-dev qcom-sensors-core qcom-sensors-core-dev qcom-sensors-registry qcom-sensors-services qcom-sensors-test-core qcom-sensors-test-core-dev qcom-sensors-test-apps +# rpm:Sensors_Validation:sns_test=qcom-sensors-api qcom-sensors-api-dev qcom-sensors-core qcom-sensors-core-dev qcom-sensors-registry qcom-sensors-services qcom-sensors-test-core qcom-sensors-test-core-dev qcom-sensors-test-apps +# opkg:Sensors_Validation:sns_test=qcom-sensors-api qcom-sensors-api-dev qcom-sensors-core qcom-sensors-core-dev qcom-sensors-registry qcom-sensors-services qcom-sensors-test-core qcom-sensors-test-core-dev qcom-sensors-test-apps + +# --------------------------------------------------------------------------- +# Optional OS-specific overrides can be added below. +# +# OS-specific mappings override provider mappings. +# +# Examples: +# qcom-distro:media-ctl=v4l-utils +# qcom-distro:Video_V4L2_Runner:yavta=qcom-yavta +# debian:modetest=libdrm-tests +# ubuntu:modetest=libdrm-tests +# centos:modetest=libdrm-tests +# --------------------------------------------------------------------------- + +# --------------------------------------------------------------------------- +# Generic package sets. +# +# Format: +# provider:package-set:=pkg1 pkg2 ... +# os-id:package-set:=pkg1 pkg2 ... +# +# These are package-level dependencies, not command dependencies. +# They are useful when a test needs a package stack even if the command is +# already present or the package is expected to be installed in the rootfs. +# --------------------------------------------------------------------------- + +#apt:package-set:fastrpc=fastrpc-support fastrpc-tests +debian:package-set:fastrpc=fastrpc-support fastrpc-tests +ubuntu:package-set:fastrpc=fastrpc-support fastrpc-tests +# Add CentOS only when rpm package names/repo are confirmed: +# centos:package-set:fastrpc=fastrpc-support fastrpc-tests diff --git a/Runner/config/pkg_provider.conf b/Runner/config/pkg_provider.conf new file mode 100755 index 00000000..203523ba --- /dev/null +++ b/Runner/config/pkg_provider.conf @@ -0,0 +1,59 @@ +# qcom-linux-testkit package provider configuration +# +# Repo-owned package recovery policy. +# +# This file controls whether check_dependencies() is allowed to recover +# missing commands by installing mapped packages through the active package +# manager. +# +# Package names are resolved from: +# +# Runner/config/pkg_command_map.conf +# +# Supported providers: +# auto +# apt +# rpm +# opkg +# check +# +# auto detects: +# apt-get + dpkg-query -> apt +# dnf/yum + rpm -> rpm +# opkg -> opkg +# none -> check + +provider=auto + +# 0 = missing dependency -> SKIP as before +# 1 = try package-provider recovery before SKIP +check_dependencies_recover=1 + +# 0 = check only +# 1 = install missing mapped packages +auto_install=1 + +# Command-to-package map, relative to Runner/ +package_map=config/pkg_command_map.conf + +# 0 = do not mutate package versions +# 1 = apt upgrade / dnf upgrade / yum upgrade before install +package_upgrade=0 + +# 0 = normal logs +# 1 = verbose provider debug logs +debug=0 + +# Optional built-in Qualcomm Debusine apt source. +# +# none = use existing apt sources only +# qli = configure /etc/apt/sources.list.d/qli.sources +# qli-staging = configure /etc/apt/sources.list.d/qli-staging.sources +apt_debusine_source=none + +# Debian suite for built-in Qualcomm Debusine apt source. +apt_debusine_suite=trixie + +# 0 = only install missing packages +# 1 = package-set helper upgrades already-installed packages in that set only +package_set_upgrade=1 diff --git a/Runner/suites/Multimedia/CDSP/fastrpc_test/run.sh b/Runner/suites/Multimedia/CDSP/fastrpc_test/run.sh index e80fd27a..d3978995 100755 --- a/Runner/suites/Multimedia/CDSP/fastrpc_test/run.sh +++ b/Runner/suites/Multimedia/CDSP/fastrpc_test/run.sh @@ -4,8 +4,10 @@ # --------- Robustly source init_env and functestlib.sh ---------- TESTNAME="fastrpc_test" -RESULT_FILE="$TESTNAME.res" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +RES_FILE="$SCRIPT_DIR/${TESTNAME}.res" +RESULT_FILE="$RES_FILE" + INIT_ENV="" SEARCH="$SCRIPT_DIR" while [ "$SEARCH" != "/" ]; do @@ -15,26 +17,38 @@ while [ "$SEARCH" != "/" ]; do fi SEARCH=$(dirname "$SEARCH") done - + if [ -z "$INIT_ENV" ]; then echo "[ERROR] Could not find init_env (starting at $SCRIPT_DIR)" >&2 - echo "$TESTNAME : FAIL" >"$RESULT_FILE" 2>/dev/null || true + echo "$TESTNAME FAIL" >"$RES_FILE" 2>/dev/null || true exit 0 fi - + # Only source once (idempotent) if [ -z "${__INIT_ENV_LOADED:-}" ]; then # shellcheck disable=SC1090 . "$INIT_ENV" __INIT_ENV_LOADED=1 fi - + # shellcheck disable=SC1090,SC1091 . "$TOOLS/functestlib.sh" - + # shellcheck disable=SC1090,SC1091 . "$TOOLS/lib_fastrpc.sh" -# --------------------------------------------------------------- + +# Optional generic package-set recovery. +# This must be a clean no-op when no package-set mapping exists for the active OS/provider. +if [ -f "$TOOLS/lib_pkg_provider.sh" ]; then + # shellcheck disable=SC1091 + . "$TOOLS/lib_pkg_provider.sh" + + if ! pkg_ensure_package_set fastrpc; then + log_skip "$TESTNAME SKIP - required package set is not available: fastrpc" + echo "$TESTNAME SKIP" >"$RES_FILE" + exit 0 + fi +fi # Defaults REPEAT=1 diff --git a/Runner/utils/functestlib.sh b/Runner/utils/functestlib.sh index 78b6c2b4..d113169b 100755 --- a/Runner/utils/functestlib.sh +++ b/Runner/utils/functestlib.sh @@ -147,33 +147,52 @@ check_dependencies() { # shellcheck disable=SC2086 set -- $1 fi - + missing=0 missing_cmds="" - + + if [ -n "${TOOLS:-}" ] && [ -f "$TOOLS/lib_pkg_provider.sh" ]; then + # shellcheck disable=SC1091 + . "$TOOLS/lib_pkg_provider.sh" + fi + for cmd in "$@"; do [ -n "$cmd" ] || continue - if ! command -v "$cmd" >/dev/null 2>&1; then - log_warn "Required command '$cmd' not found in PATH." - missing=1 - missing_cmds="$missing_cmds $cmd" + + if command -v "$cmd" >/dev/null 2>&1; then + continue fi + + if command -v pkg_check_dependencies_recover_enabled >/dev/null 2>&1; then + if pkg_check_dependencies_recover_enabled; then + if pkg_ensure_command "$cmd"; then + if command -v "$cmd" >/dev/null 2>&1; then + continue + fi + fi + fi + fi + + log_warn "Required command '$cmd' not found in PATH." + missing=1 + missing_cmds="$missing_cmds $cmd" done - + if [ "$missing" -ne 0 ]; then testname="${TESTNAME:-UnknownTest}" log_skip "$testname SKIP missing dependencies$missing_cmds" if [ -n "${TESTNAME:-}" ]; then echo "$TESTNAME SKIP" > "./$TESTNAME.res" 2>/dev/null || true fi - - # Default: exit like today. Allow opt-out for callers that want a return code. + + # Preserve existing escape hatch for callers that expect return code. if [ "${CHECK_DEPS_NO_EXIT:-0}" = "1" ]; then return 1 fi + exit 0 fi - + return 0 } diff --git a/Runner/utils/lib_pkg_provider.sh b/Runner/utils/lib_pkg_provider.sh new file mode 100755 index 00000000..fe36afe6 --- /dev/null +++ b/Runner/utils/lib_pkg_provider.sh @@ -0,0 +1,1575 @@ +#!/bin/sh + +# Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. +# SPDX-License-Identifier: BSD-3-Clause + +# Optional package provider abstraction for qcom-linux-testkit. +# +# Provider model: +# apt - apt-get/dpkg-query based systems +# rpm - dnf/yum/rpm based systems, including Yocto images configured with dnf +# opkg - opkg based systems +# check - check-only fallback when no supported package manager is available +# +# Package names are resolved through Runner/config/pkg_command_map.conf. +# The library intentionally avoids command-name guessing because command names +# and distro package names often differ. +# +# For apt systems, optional *.sources artifacts under /opt/qcom-testkit/metadata +# are copied into /etc/apt/sources.list.d/ when present. Optional apt auth is +# created only when secure target-local secret files are present. +# +# Package-manager command output is printed to stdout to simplify CI debugging. + +PKG_PROVIDER="auto" +PKG_CHECK_DEPS_RECOVER="1" +PKG_AUTO_INSTALL="1" +PKG_PACKAGE_MAP="config/pkg_command_map.conf" +PKG_PACKAGE_UPGRADE="0" +PKG_DEBUG="0" + +PKG_APT_DEBUSINE_SOURCE="${PKG_APT_DEBUSINE_SOURCE:-none}" +PKG_APT_DEBUSINE_SUITE="${PKG_APT_DEBUSINE_SUITE:-auto}" +PKG_PACKAGE_SET_UPGRADE="${PKG_PACKAGE_SET_UPGRADE:-1}" + +PKG_NETWORK_REQUIRED="1" +PKG_NETWORK_RECOVER="1" +PKG_NETWORK_RETRIES="2" +PKG_NETWORK_RETRY_SLEEP="5" + +PKG_COMMAND_RETRIES="2" +PKG_COMMAND_RETRY_SLEEP="5" + +PKG_APT_GET="apt-get" +PKG_APT_INSTALL_RECOMMENDS="0" +PKG_APT_LOCK_TIMEOUT="120" +PKG_APT_FIX_BROKEN="1" +PKG_APT_FIX_MISSING="1" +PKG_APT_UPDATED_MARK="/tmp/qcom_testkit_apt_updated" +PKG_APT_UPGRADED_MARK="/tmp/qcom_testkit_apt_upgraded" + +PKG_APT_SOURCES_ARTIFACT_DIR="/opt/qcom-testkit/metadata" +PKG_APT_AUTH_CONF="/etc/apt/auth.conf.d/debusine-ci-auth.conf" +PKG_APT_AUTH_MACHINE_FALLBACK="deb.debusine.qualcomm.com" +PKG_APT_AUTH_LOGIN_FILE="/run/qcom-testkit/secrets/debusine_login" +PKG_APT_AUTH_PASSWORD_FILE="/run/qcom-testkit/secrets/debusine_api_token" + +PKG_RPM_UPDATED_MARK="/tmp/qcom_testkit_rpm_updated" +PKG_RPM_UPGRADED_MARK="/tmp/qcom_testkit_rpm_upgraded" +PKG_RPM_BEST_EFFORT_CLEAN="1" + +PKG_OPKG_UPDATED_MARK="/tmp/qcom_testkit_opkg_updated" + +__PKG_PROVIDER_INITIALIZED="0" +__PKG_ACTIVE_PROVIDER="" + +# Log an informational message using functestlib.sh logging when available. +pkg_log_info() { + if command -v log_info >/dev/null 2>&1; then + log_info "$@" + else + printf '[INFO] %s\n' "$*" + fi +} + +# Log a PASS message using functestlib.sh logging when available. +pkg_log_pass() { + if command -v log_pass >/dev/null 2>&1; then + log_pass "$@" + else + printf '[PASS] %s\n' "$*" + fi +} + +# Log a warning message using functestlib.sh logging when available. +pkg_log_warn() { + if command -v log_warn >/dev/null 2>&1; then + log_warn "$@" + else + printf '[WARN] %s\n' "$*" + fi +} + +# Log a failure message using functestlib.sh logging when available. +pkg_log_fail() { + if command -v log_fail >/dev/null 2>&1; then + log_fail "$@" + else + printf '[FAIL] %s\n' "$*" + fi +} + +# Print provider debug messages only when debug=1 is enabled. +pkg_debug() { + if [ "$PKG_DEBUG" = "1" ]; then + pkg_log_info "[PKG] $*" + fi +} + +# Return success when a config value represents boolean true. +pkg_bool_true() { + bool_value="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')" + + case "$bool_value" in + 1|yes|true|on|enable|enabled) + return 0 + ;; + *) + return 1 + ;; + esac +} + +# Return success when the supplied value is an unsigned integer. +pkg_is_uint() { + case "$1" in + ''|*[!0-9]*) + return 1 + ;; + *) + return 0 + ;; + esac +} + +# Normalize a numeric value. +pkg_normalize_uint() { + uint_value="$1" + uint_fallback="$2" + + if pkg_is_uint "$uint_value"; then + printf '%s\n' "$uint_value" + else + printf '%s\n' "$uint_fallback" + fi +} + +# Trim whitespace and optional single/double quotes from a config value. +pkg_strip_value() { + printf '%s' "$1" | + sed 's/^[[:space:]]*//; s/[[:space:]]*$//' | + sed 's/^"//; s/"$//' | + sed "s/^'//; s/'$//" +} + +# Apply one key=value entry from pkg_provider.conf. +pkg_apply_config_entry() { + cfg_key="$1" + cfg_value="$2" + + case "$cfg_key" in + provider) + PKG_PROVIDER="$cfg_value" + ;; + check_dependencies_recover) + PKG_CHECK_DEPS_RECOVER="$cfg_value" + ;; + auto_install) + PKG_AUTO_INSTALL="$cfg_value" + ;; + package_map) + PKG_PACKAGE_MAP="$cfg_value" + ;; + package_upgrade) + PKG_PACKAGE_UPGRADE="$cfg_value" + ;; + debug) + PKG_DEBUG="$cfg_value" + ;; + apt_debusine_source) + PKG_APT_DEBUSINE_SOURCE="$cfg_value" + ;; + apt_debusine_suite) + PKG_APT_DEBUSINE_SUITE="$cfg_value" + ;; + package_set_upgrade) + PKG_PACKAGE_SET_UPGRADE="$cfg_value" + ;; + *) + pkg_debug "Ignoring unknown package provider config key, $cfg_key" + ;; + esac +} + +# Load pkg_provider.conf and apply supported key=value entries. +pkg_load_config_file() { + cfg_file="$1" + + if [ -z "$cfg_file" ] || [ ! -r "$cfg_file" ]; then + return 1 + fi + + while IFS= read -r cfg_line || [ -n "$cfg_line" ]; do + case "$cfg_line" in + ''|'#'*) + continue + ;; + *=*) + cfg_key="$(printf '%s' "${cfg_line%%=*}" | tr -d '[:space:]')" + cfg_value="$(pkg_strip_value "${cfg_line#*=}")" + [ -n "$cfg_key" ] || continue + pkg_apply_config_entry "$cfg_key" "$cfg_value" + ;; + *) + pkg_debug "Ignoring malformed config line, $cfg_line" + ;; + esac + done < "$cfg_file" + + return 0 +} + +# Locate the repo-owned package provider config file. +pkg_default_config_path() { + if [ -n "${ROOT_DIR:-}" ] && [ -r "$ROOT_DIR/config/pkg_provider.conf" ]; then + printf '%s\n' "$ROOT_DIR/config/pkg_provider.conf" + return 0 + fi + + if [ -n "${TOOLS:-}" ] && [ -r "$TOOLS/../config/pkg_provider.conf" ]; then + printf '%s\n' "$TOOLS/../config/pkg_provider.conf" + return 0 + fi + + return 1 +} + +# Initialize the package provider once per shell process. +pkg_provider_init() { + if [ "$__PKG_PROVIDER_INITIALIZED" = "1" ]; then + return 0 + fi + + provider_cfg_file="$(pkg_default_config_path || true)" + if [ -n "$provider_cfg_file" ]; then + pkg_load_config_file "$provider_cfg_file" || true + else + pkg_debug "No package provider config found, using built-in defaults" + fi + + __PKG_PROVIDER_INITIALIZED="1" + __PKG_ACTIVE_PROVIDER="" + return 0 +} + +# Return success when dependency recovery is enabled. +pkg_check_dependencies_recover_enabled() { + pkg_provider_init + pkg_bool_true "$PKG_CHECK_DEPS_RECOVER" +} + +# Return success when package installation is allowed and current user is root. +pkg_can_install() { + pkg_provider_init + + if ! pkg_bool_true "$PKG_AUTO_INSTALL"; then + pkg_debug "auto_install disabled" + return 1 + fi + + current_uid="$(id -u 2>/dev/null || echo 1)" + if [ "$current_uid" -ne 0 ] 2>/dev/null; then + pkg_log_warn "Package install requested but current user is not root" + return 1 + fi + + return 0 +} + +# Read a normalized value from /etc/os-release. +pkg_os_release_value() { + os_key="$1" + + if [ ! -r /etc/os-release ]; then + return 1 + fi + + sed -n "s/^${os_key}=//p" /etc/os-release 2>/dev/null | + sed -n '1p' | + sed 's/^"//; s/"$//' | + tr '[:upper:]' '[:lower:]' +} + +# Detect OS ID for logging and package-map override lookup. +pkg_detect_os_id() { + detected_os_id="$(pkg_os_release_value ID || true)" + + if [ -n "$detected_os_id" ]; then + printf '%s\n' "$detected_os_id" + return 0 + fi + + printf '%s\n' "unknown" + return 0 +} + +# Detect package-manager provider. +pkg_detect_provider() { + if command -v apt-get >/dev/null 2>&1 && command -v dpkg-query >/dev/null 2>&1; then + printf '%s\n' "apt" + return 0 + fi + + if command -v rpm >/dev/null 2>&1; then + if command -v dnf >/dev/null 2>&1 || command -v yum >/dev/null 2>&1; then + printf '%s\n' "rpm" + return 0 + fi + fi + + if command -v opkg >/dev/null 2>&1; then + printf '%s\n' "opkg" + return 0 + fi + + printf '%s\n' "check" + return 0 +} + +# Return effective provider. +pkg_effective_provider() { + pkg_provider_init + + if [ "$PKG_PROVIDER" = "auto" ]; then + pkg_detect_provider + return 0 + fi + + printf '%s\n' "$PKG_PROVIDER" + return 0 +} + +# Return cached active provider. +pkg_active_provider() { + pkg_provider_init + + if [ -z "$__PKG_ACTIVE_PROVIDER" ]; then + __PKG_ACTIVE_PROVIDER="$(pkg_effective_provider)" + pkg_debug "active provider=$__PKG_ACTIVE_PROVIDER" + fi + + printf '%s\n' "$__PKG_ACTIVE_PROVIDER" +} + +# Return success when a command is available. +pkg_have_command() { + command -v "$1" >/dev/null 2>&1 +} + +# Resolve absolute or repo-relative path. +pkg_resolve_path() { + resolve_path="$1" + + case "$resolve_path" in + /*) + printf '%s\n' "$resolve_path" + ;; + *) + if [ -n "${ROOT_DIR:-}" ] && [ -e "$ROOT_DIR/$resolve_path" ]; then + printf '%s\n' "$ROOT_DIR/$resolve_path" + return 0 + fi + + if [ -n "${TOOLS:-}" ] && [ -e "$TOOLS/../$resolve_path" ]; then + printf '%s\n' "$TOOLS/../$resolve_path" + return 0 + fi + + if [ -n "${ROOT_DIR:-}" ]; then + printf '%s\n' "$ROOT_DIR/$resolve_path" + else + printf '%s\n' "$resolve_path" + fi + ;; + esac +} + +# Look up exact key in command-to-package map. +pkg_lookup_key_in_map() { + lookup_map_file="$1" + lookup_map_key="$2" + + while IFS= read -r lookup_line || [ -n "$lookup_line" ]; do + case "$lookup_line" in + ''|'#'*) + continue + ;; + *=*) + lookup_key="$(printf '%s' "${lookup_line%%=*}" | tr -d '[:space:]')" + lookup_value="$(pkg_strip_value "${lookup_line#*=}")" + + if [ "$lookup_key" = "$lookup_map_key" ]; then + printf '%s\n' "$lookup_value" + return 0 + fi + ;; + esac + done < "$lookup_map_file" + + return 1 +} + +# Resolve command to package names using map. +pkg_lookup_packages_for_command() { + lookup_cmd="$1" + lookup_provider="$(pkg_active_provider)" + lookup_os_id="$(pkg_detect_os_id)" + lookup_testname="${TESTNAME:-}" + lookup_map_file="$(pkg_resolve_path "$PKG_PACKAGE_MAP")" + + if [ ! -r "$lookup_map_file" ]; then + pkg_log_warn "Package map file is not readable, $lookup_map_file" + return 1 + fi + + if [ -n "$lookup_testname" ]; then + lookup_value="$(pkg_lookup_key_in_map "$lookup_map_file" "${lookup_os_id}:${lookup_testname}:${lookup_cmd}" || true)" + if [ -n "$lookup_value" ]; then + printf '%s\n' "$lookup_value" + return 0 + fi + + lookup_value="$(pkg_lookup_key_in_map "$lookup_map_file" "${lookup_provider}:${lookup_testname}:${lookup_cmd}" || true)" + if [ -n "$lookup_value" ]; then + printf '%s\n' "$lookup_value" + return 0 + fi + fi + + lookup_value="$(pkg_lookup_key_in_map "$lookup_map_file" "${lookup_os_id}:${lookup_cmd}" || true)" + if [ -n "$lookup_value" ]; then + printf '%s\n' "$lookup_value" + return 0 + fi + + lookup_value="$(pkg_lookup_key_in_map "$lookup_map_file" "${lookup_provider}:${lookup_cmd}" || true)" + if [ -n "$lookup_value" ]; then + printf '%s\n' "$lookup_value" + return 0 + fi + + pkg_log_warn "No package mapping found for command, os=$lookup_os_id provider=$lookup_provider cmd=$lookup_cmd" + return 1 +} + +# Look up a named package set from OS/provider package map without logging to stdout. +pkg_lookup_package_set() { + set_name="$1" + set_provider="$(pkg_active_provider)" + set_os_id="$(pkg_detect_os_id)" + set_map_file="$(pkg_resolve_path "$PKG_PACKAGE_MAP")" + + [ -n "$set_name" ] || return 1 + [ -r "$set_map_file" ] || return 1 + + set_value="$(pkg_lookup_key_in_map "$set_map_file" "${set_os_id}:package-set:${set_name}" || true)" + if [ -n "$set_value" ]; then + printf '%s\n' "$set_value" + return 0 + fi + + set_value="$(pkg_lookup_key_in_map "$set_map_file" "${set_provider}:package-set:${set_name}" || true)" + if [ -n "$set_value" ]; then + printf '%s\n' "$set_value" + return 0 + fi + + return 1 +} + +# Return installed package version for the active package provider. +pkg_installed_package_version() { + version_pkg="$1" + version_provider="$(pkg_active_provider)" + + [ -n "$version_pkg" ] || return 1 + + case "$version_provider" in + apt) + dpkg-query -W -f='${Version}\n' "$version_pkg" 2>/dev/null | sed -n '1p' + ;; + rpm) + rpm -q --qf '%{VERSION}-%{RELEASE}\n' "$version_pkg" 2>/dev/null | sed -n '1p' + ;; + opkg) + opkg status "$version_pkg" 2>/dev/null | + sed -n 's/^Version:[[:space:]]*//p' | + sed -n '1p' + ;; + *) + return 1 + ;; + esac +} + +# Log package presence and include installed version when available. +pkg_log_package_present() { + present_pkg="$1" + present_version="$(pkg_installed_package_version "$present_pkg" || true)" + + if [ -n "$present_version" ]; then + pkg_log_pass "Package already installed, $present_pkg version=$present_version" + else + pkg_log_pass "Package already installed, $present_pkg" + fi +} + +# Return success when package is installed. +pkg_have_package() { + have_pkg="$1" + have_provider="$(pkg_active_provider)" + + [ -n "$have_pkg" ] || return 1 + + case "$have_provider" in + apt) + dpkg-query -W -f='${Status}\n' "$have_pkg" 2>/dev/null | + grep -q "install ok installed" + ;; + rpm) + rpm -q "$have_pkg" >/dev/null 2>&1 + ;; + opkg) + opkg status "$have_pkg" 2>/dev/null | + grep -q "Status:.* installed" + ;; + *) + return 1 + ;; + esac +} + +# Run command and print result. +pkg_run_cmd() { + cmd_label="$1" + shift + + pkg_log_info "Running command [$cmd_label]: $*" + + "$@" + cmd_rc=$? + + if [ "$cmd_rc" -eq 0 ]; then + pkg_log_pass "Command passed [$cmd_label]" + else + pkg_log_warn "Command failed [$cmd_label], rc=$cmd_rc" + fi + + return "$cmd_rc" +} + +# Run command with bounded retries. +pkg_run_cmd_retry() { + retry_label="$1" + shift + + retry_count="$(pkg_normalize_uint "$PKG_COMMAND_RETRIES" 2)" + retry_sleep="$(pkg_normalize_uint "$PKG_COMMAND_RETRY_SLEEP" 5)" + + if [ "$retry_count" -lt 1 ]; then + retry_count=1 + fi + + retry_attempt=1 + while [ "$retry_attempt" -le "$retry_count" ]; do + pkg_log_info "Command attempt [$retry_label], ${retry_attempt}/${retry_count}" + + if pkg_run_cmd "$retry_label" "$@"; then + return 0 + fi + + if [ "$retry_attempt" -lt "$retry_count" ]; then + pkg_log_warn "Retrying command [$retry_label] after ${retry_sleep}s" + sleep "$retry_sleep" + fi + + retry_attempt=$((retry_attempt + 1)) + done + + pkg_log_fail "Command failed after ${retry_count} attempt(s) [$retry_label]" + return 1 +} + +# Check network using functestlib helpers when available. +pkg_network_status() { + if command -v check_network_status >/dev/null 2>&1; then + check_network_status + net_rc=$? + + if [ "$net_rc" -eq 0 ]; then + return 0 + fi + + if [ "$net_rc" -eq 2 ]; then + pkg_log_warn "Network has IP but internet probe failed; internal mirrors may still work" + return 0 + fi + + return 1 + fi + + if command -v ip >/dev/null 2>&1; then + if ip -4 route get 1.1.1.1 >/dev/null 2>&1; then + pkg_log_pass "Network route exists" + return 0 + fi + fi + + pkg_log_warn "Unable to confirm network availability" + return 1 +} + +# Try network recovery using existing functestlib helpers. +pkg_try_network_recovery_once() { + pkg_log_info "Attempting package-provider network recovery" + + if command -v ensure_network_online >/dev/null 2>&1; then + ensure_network_online || true + return 0 + fi + + if command -v get_ethernet_interfaces >/dev/null 2>&1 && + command -v bringup_interface >/dev/null 2>&1; then + net_interfaces="$(get_ethernet_interfaces 2>/dev/null || true)" + + for net_iface in $net_interfaces; do + [ -n "$net_iface" ] || continue + pkg_log_info "Trying Ethernet bring-up, iface=$net_iface" + bringup_interface "$net_iface" || true + + if pkg_network_status; then + return 0 + fi + done + fi + + pkg_log_warn "No supported network recovery helper succeeded" + return 1 +} + +# Ensure network before package operations. +pkg_ensure_network_ready() { + if ! pkg_bool_true "$PKG_NETWORK_REQUIRED"; then + return 0 + fi + + if pkg_network_status; then + pkg_log_pass "Network is ready for package operations" + return 0 + fi + + if ! pkg_bool_true "$PKG_NETWORK_RECOVER"; then + pkg_log_fail "Network is not ready and recovery is disabled" + return 1 + fi + + net_retries="$(pkg_normalize_uint "$PKG_NETWORK_RETRIES" 2)" + net_retry_sleep="$(pkg_normalize_uint "$PKG_NETWORK_RETRY_SLEEP" 5)" + + if [ "$net_retries" -lt 1 ]; then + net_retries=1 + fi + + net_attempt=1 + while [ "$net_attempt" -le "$net_retries" ]; do + pkg_log_warn "Network recovery attempt ${net_attempt}/${net_retries}" + + pkg_try_network_recovery_once || true + sleep "$net_retry_sleep" + + if pkg_network_status; then + pkg_log_pass "Network recovered successfully" + return 0 + fi + + net_attempt=$((net_attempt + 1)) + done + + pkg_log_fail "Network is still not ready after recovery attempts" + return 1 +} + +# Read first line from file. +pkg_read_first_line() { + read_file_path="$1" + + if [ -z "$read_file_path" ] || [ ! -r "$read_file_path" ]; then + return 1 + fi + + sed -n '1p' "$read_file_path" 2>/dev/null | + sed 's/^[[:space:]]*//; s/[[:space:]]*$//' +} + +# Detect apt auth machine from *.sources, fallback to configured hostname. +pkg_apt_detect_auth_machine() { + if [ -d "$PKG_APT_SOURCES_ARTIFACT_DIR" ]; then + for auth_src_file in "$PKG_APT_SOURCES_ARTIFACT_DIR"/*.sources; do + [ -r "$auth_src_file" ] || continue + + auth_machine="$( + sed -n 's#.*https://\([^/[:space:]]*\).*#\1#p' "$auth_src_file" | + sed -n '1p' + )" + + if [ -n "$auth_machine" ]; then + printf '%s\n' "$auth_machine" + return 0 + fi + done + fi + + printf '%s\n' "$PKG_APT_AUTH_MACHINE_FALLBACK" + return 0 +} + +# Read optional apt auth login. +pkg_apt_read_auth_login() { + pkg_read_first_line "$PKG_APT_AUTH_LOGIN_FILE" +} + +# Read optional apt auth password/token. +pkg_apt_read_auth_password() { + pkg_read_first_line "$PKG_APT_AUTH_PASSWORD_FILE" +} + +# Create optional apt auth config if secret files are present. +pkg_apt_install_auth_if_secrets_present() { + if [ ! -r "$PKG_APT_AUTH_LOGIN_FILE" ] || [ ! -r "$PKG_APT_AUTH_PASSWORD_FILE" ]; then + pkg_log_info "APT auth secret files not present, skipping auth config creation" + return 0 + fi + + apt_auth_login="$(pkg_apt_read_auth_login || true)" + apt_auth_password="$(pkg_apt_read_auth_password || true)" + apt_auth_machine="$(pkg_apt_detect_auth_machine)" + + if [ -z "$apt_auth_login" ] || [ -z "$apt_auth_password" ]; then + pkg_log_warn "APT auth secret file is empty, skipping auth config creation" + return 0 + fi + + apt_auth_dir="$(dirname "$PKG_APT_AUTH_CONF")" + + if [ ! -d "$apt_auth_dir" ]; then + mkdir -p "$apt_auth_dir" || { + pkg_log_fail "Failed to create APT auth directory, $apt_auth_dir" + return 1 + } + fi + + apt_tmp_auth="${PKG_APT_AUTH_CONF}.$$" + + apt_old_umask="$(umask)" + umask 077 + + { + printf 'machine %s\n' "$apt_auth_machine" + printf 'login %s\n' "$apt_auth_login" + printf 'password %s\n' "$apt_auth_password" + } > "$apt_tmp_auth" || { + umask "$apt_old_umask" + rm -f "$apt_tmp_auth" + pkg_log_fail "Failed to write temporary APT auth config" + return 1 + } + + umask "$apt_old_umask" + + chmod 600 "$apt_tmp_auth" 2>/dev/null || true + + mv "$apt_tmp_auth" "$PKG_APT_AUTH_CONF" || { + rm -f "$apt_tmp_auth" + pkg_log_fail "Failed to install APT auth config, $PKG_APT_AUTH_CONF" + return 1 + } + + chmod 600 "$PKG_APT_AUTH_CONF" 2>/dev/null || true + + pkg_log_pass "Installed optional APT auth config, $PKG_APT_AUTH_CONF" + return 0 +} + +# Install optional apt source artifacts when present. +pkg_apt_install_sources_if_present() { + if [ ! -d "$PKG_APT_SOURCES_ARTIFACT_DIR" ]; then + pkg_log_info "No APT metadata source directory found, using existing apt sources" + return 0 + fi + + if [ ! -d /etc/apt/sources.list.d ]; then + mkdir -p /etc/apt/sources.list.d || { + pkg_log_fail "Failed to create /etc/apt/sources.list.d" + return 1 + } + fi + + apt_src_found=0 + + for apt_src_file in "$PKG_APT_SOURCES_ARTIFACT_DIR"/*.sources; do + [ -r "$apt_src_file" ] || continue + apt_src_found=1 + + apt_target_path="/etc/apt/sources.list.d/$(basename "$apt_src_file")" + + if grep -qi "password" "$apt_src_file" 2>/dev/null; then + pkg_log_fail "APT source artifact appears to contain credentials, refusing to install, $apt_src_file" + return 1 + fi + + cp "$apt_src_file" "$apt_target_path" || { + pkg_log_fail "Failed to install APT source file, $apt_target_path" + return 1 + } + + chmod 644 "$apt_target_path" 2>/dev/null || true + pkg_log_pass "Installed APT source file, $apt_target_path" + done + + if [ "$apt_src_found" -eq 0 ]; then + pkg_log_info "No *.sources artifacts found, using existing apt sources" + fi + + return 0 +} + +# Log metadata source-package, if present. +pkg_apt_log_metadata_source_package() { + apt_metadata_file="$PKG_APT_SOURCES_ARTIFACT_DIR/metadata.json" + + if [ ! -r "$apt_metadata_file" ]; then + return 0 + fi + + apt_source_package="$( + sed -n 's/.*"source-package"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$apt_metadata_file" | + sed -n '1p' + )" + + if [ -n "$apt_source_package" ]; then + pkg_log_info "APT metadata source-package, $apt_source_package" + fi + + return 0 +} + +# Return the default Qualcomm Debusine suite for the detected OS. +pkg_apt_default_debusine_suite() { + debusine_os_id="$(pkg_detect_os_id)" + + case "$debusine_os_id" in + ubuntu) + printf '%s\n' "resolute" + ;; + debian) + printf '%s\n' "trixie" + ;; + *) + printf '%s\n' "trixie" + ;; + esac +} + +# Return success when an apt source file already has the requested URI and suite. +pkg_apt_source_file_matches() { + source_file="$1" + expected_uri="$2" + expected_suite="$3" + + [ -r "$source_file" ] || return 1 + + expected_uri_norm="$(printf '%s' "$expected_uri" | sed 's#/*$##')" + actual_uri_norm="$( + sed -n 's/^URIs:[[:space:]]*//p' "$source_file" 2>/dev/null | + sed -n '1p' | + sed 's#/*$##' + )" + actual_suite="$( + sed -n 's/^Suites:[[:space:]]*//p' "$source_file" 2>/dev/null | + sed -n '1p' + )" + + [ "$actual_uri_norm" = "$expected_uri_norm" ] && + [ "$actual_suite" = "$expected_suite" ] +} + +# Configure a built-in Qualcomm Debusine apt source when requested. +pkg_apt_install_debusine_source_if_configured() { + debusine_source_name="$PKG_APT_DEBUSINE_SOURCE" + + case "$PKG_APT_DEBUSINE_SUITE" in + ''|auto) + debusine_suite="$(pkg_apt_default_debusine_suite)" + ;; + *) + debusine_suite="$PKG_APT_DEBUSINE_SUITE" + ;; + esac + + case "$debusine_source_name" in + ""|none|disabled) + pkg_log_info "No built-in Qualcomm Debusine apt source requested" + return 0 + ;; + qli) + debusine_target="/etc/apt/sources.list.d/qli.sources" + debusine_uri="https://deb.debusine.qualcomm.com/qualcomm/qli" + debusine_components="main contrib non-free-firmware non-free" + ;; + qli-staging) + debusine_target="/etc/apt/sources.list.d/qli-staging.sources" + debusine_uri="https://deb.debusine.qualcomm.com/qualcomm/qli-staging" + debusine_components="main contrib non-free non-free-firmware" + ;; + *) + pkg_log_fail "Unsupported apt_debusine_source value, $debusine_source_name" + return 1 + ;; + esac + + mkdir -p /etc/apt/sources.list.d || { + pkg_log_fail "Failed to create /etc/apt/sources.list.d" + return 1 + } + + if pkg_apt_source_file_matches "$debusine_target" "$debusine_uri" "$debusine_suite"; then + pkg_log_pass "APT source already configured, $debusine_target" + return 0 + fi + + if [ -r "$debusine_target" ]; then + pkg_log_warn "APT source exists but does not match requested URI/suite, updating, $debusine_target" + fi + + case "$debusine_source_name" in + qli) + cat > "$debusine_target" < "$debusine_target" </dev/null || true + pkg_log_pass "Installed Qualcomm Debusine apt source, $debusine_target" + return 0 +} + +# Prepare apt sources and optional auth. +pkg_apt_prepare_sources() { + pkg_apt_install_debusine_source_if_configured || return 1 + pkg_apt_install_sources_if_present || return 1 + pkg_apt_install_auth_if_secrets_present || return 1 + pkg_apt_log_metadata_source_package || true + return 0 +} + +# Dump file with credential-like fields redacted. +pkg_dump_file_redacted() { + dump_prefix="$1" + dump_file_path="$2" + + if [ ! -r "$dump_file_path" ]; then + return 0 + fi + + sed \ + -e 's/[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd][[:space:]]\+.*/password REDACTED/' \ + -e 's/[Ll][Oo][Gg][Ii][Nn][[:space:]]\+.*/login REDACTED/' \ + -e 's#://[^/@][^/@]*:[^/@][^/@]*@#://REDACTED:REDACTED@#g' \ + "$dump_file_path" 2>/dev/null | + sed "s/^/[$dump_prefix] /" +} + +# Dump apt sources for CI debugging. +pkg_apt_dump_sources() { + pkg_log_info "APT configured sources:" + + if [ -r /etc/apt/sources.list ]; then + pkg_log_info "APT source file, /etc/apt/sources.list" + pkg_dump_file_redacted "APT-SOURCE" /etc/apt/sources.list || true + fi + + if [ -d /etc/apt/sources.list.d ]; then + find /etc/apt/sources.list.d -maxdepth 1 -type f -print 2>/dev/null | + while IFS= read -r dump_src_file; do + pkg_log_info "APT source file, $dump_src_file" + pkg_dump_file_redacted "APT-SOURCE" "$dump_src_file" || true + done + fi +} + +# Verify apt-get availability. +pkg_apt_tool_check() { + if ! command -v "$PKG_APT_GET" >/dev/null 2>&1; then + pkg_log_fail "$PKG_APT_GET is not available" + return 1 + fi + + return 0 +} + +# Run apt update once per test session. +pkg_apt_update() { + pkg_apt_tool_check || return 1 + + if [ -f "$PKG_APT_UPDATED_MARK" ]; then + pkg_log_info "APT update already completed in this test session" + return 0 + fi + + pkg_apt_prepare_sources || return 1 + pkg_ensure_network_ready || return 1 + + pkg_log_info "APT version:" + "$PKG_APT_GET" --version 2>&1 || true + + pkg_apt_dump_sources || true + + pkg_run_cmd_retry "apt-update" \ + env DEBIAN_FRONTEND=noninteractive \ + "$PKG_APT_GET" update \ + -o Acquire::Retries=2 \ + -o "DPkg::Lock::Timeout=${PKG_APT_LOCK_TIMEOUT}" + + apt_update_rc=$? + + if [ "$apt_update_rc" -eq 0 ]; then + : > "$PKG_APT_UPDATED_MARK" 2>/dev/null || true + fi + + return "$apt_update_rc" +} + +# Optionally run apt upgrade. +pkg_apt_upgrade() { + if ! pkg_bool_true "$PKG_PACKAGE_UPGRADE"; then + pkg_log_info "APT upgrade disabled by config" + return 0 + fi + + if [ -f "$PKG_APT_UPGRADED_MARK" ]; then + pkg_log_info "APT upgrade already completed in this test session" + return 0 + fi + + pkg_apt_update || return 1 + + pkg_run_cmd_retry "apt-upgrade" \ + env DEBIAN_FRONTEND=noninteractive \ + "$PKG_APT_GET" upgrade -y \ + -o "DPkg::Lock::Timeout=${PKG_APT_LOCK_TIMEOUT}" + + apt_upgrade_rc=$? + + if [ "$apt_upgrade_rc" -eq 0 ]; then + : > "$PKG_APT_UPGRADED_MARK" 2>/dev/null || true + fi + + return "$apt_upgrade_rc" +} + +# Best-effort apt fix-broken. +pkg_apt_fix_broken_if_enabled() { + if ! pkg_bool_true "$PKG_APT_FIX_BROKEN"; then + return 0 + fi + + pkg_run_cmd_retry "apt-fix-broken" \ + env DEBIAN_FRONTEND=noninteractive \ + "$PKG_APT_GET" -f install -y \ + -o "DPkg::Lock::Timeout=${PKG_APT_LOCK_TIMEOUT}" || true + + return 0 +} + +# List direct runtime dependencies for an apt package. +pkg_apt_package_runtime_deps() { + apt_dep_pkg="$1" + + apt-cache depends "$apt_dep_pkg" 2>/dev/null | + sed -n \ + -e 's/^[[:space:]]*Depends:[[:space:]]*//p' \ + -e 's/^[[:space:]]*PreDepends:[[:space:]]*//p' | + sed 's/<[^>]*>//g' | + awk '{print $1}' | + sed '/^$/d' | + sort -u +} + +# Log runtime dependencies and installed versions for a package. +pkg_log_package_dependencies() { + parent_pkg="$1" + dep_provider="$(pkg_active_provider)" + + [ -n "$parent_pkg" ] || return 0 + + case "$dep_provider" in + apt) + dep_list="$(pkg_apt_package_runtime_deps "$parent_pkg" || true)" + ;; + *) + return 0 + ;; + esac + + [ -n "$dep_list" ] || return 0 + + dep_list_one_line="$(printf '%s\n' "$dep_list" | tr '\n' ' ' | sed 's/[[:space:]]*$//')" + pkg_log_info "Runtime dependencies for package, $parent_pkg: $dep_list_one_line" + + for dep_pkg in $dep_list; do + [ -n "$dep_pkg" ] || continue + + if pkg_have_package "$dep_pkg"; then + dep_version="$(pkg_installed_package_version "$dep_pkg" || true)" + if [ -n "$dep_version" ]; then + pkg_log_pass "Dependency installed, parent=$parent_pkg dep=$dep_pkg version=$dep_version" + else + pkg_log_pass "Dependency installed, parent=$parent_pkg dep=$dep_pkg" + fi + else + pkg_log_warn "Dependency not installed, parent=$parent_pkg dep=$dep_pkg" + fi + done +} + +# Install apt package. +pkg_apt_install_package() { + apt_install_pkg="$1" + + pkg_apt_update || return 1 + pkg_apt_upgrade || return 1 + pkg_apt_fix_broken_if_enabled || true + + if command -v apt-cache >/dev/null 2>&1; then + apt-cache policy "$apt_install_pkg" 2>&1 | sed "s/^/[APT-POLICY:$apt_install_pkg] /" || true + fi + + if pkg_bool_true "$PKG_APT_INSTALL_RECOMMENDS"; then + if pkg_bool_true "$PKG_APT_FIX_MISSING"; then + pkg_run_cmd_retry "apt-install-$apt_install_pkg" \ + env DEBIAN_FRONTEND=noninteractive \ + "$PKG_APT_GET" install -y \ + --fix-missing \ + -o "DPkg::Lock::Timeout=${PKG_APT_LOCK_TIMEOUT}" \ + "$apt_install_pkg" + else + pkg_run_cmd_retry "apt-install-$apt_install_pkg" \ + env DEBIAN_FRONTEND=noninteractive \ + "$PKG_APT_GET" install -y \ + -o "DPkg::Lock::Timeout=${PKG_APT_LOCK_TIMEOUT}" \ + "$apt_install_pkg" + fi + else + if pkg_bool_true "$PKG_APT_FIX_MISSING"; then + pkg_run_cmd_retry "apt-install-$apt_install_pkg" \ + env DEBIAN_FRONTEND=noninteractive \ + "$PKG_APT_GET" install -y \ + --no-install-recommends \ + --fix-missing \ + -o "DPkg::Lock::Timeout=${PKG_APT_LOCK_TIMEOUT}" \ + "$apt_install_pkg" + else + pkg_run_cmd_retry "apt-install-$apt_install_pkg" \ + env DEBIAN_FRONTEND=noninteractive \ + "$PKG_APT_GET" install -y \ + --no-install-recommends \ + -o "DPkg::Lock::Timeout=${PKG_APT_LOCK_TIMEOUT}" \ + "$apt_install_pkg" + fi + fi +} + +# Pick rpm package manager. +pkg_rpm_tool() { + if command -v dnf >/dev/null 2>&1; then + printf '%s\n' "dnf" + return 0 + fi + + if command -v yum >/dev/null 2>&1; then + printf '%s\n' "yum" + return 0 + fi + + return 1 +} + +# Refresh rpm metadata. +pkg_rpm_update() { + rpm_tool="$(pkg_rpm_tool || true)" + + if [ -z "$rpm_tool" ]; then + pkg_log_warn "Neither dnf nor yum is available" + return 1 + fi + + if [ -f "$PKG_RPM_UPDATED_MARK" ]; then + pkg_log_info "$rpm_tool metadata update already completed in this test session" + return 0 + fi + + pkg_ensure_network_ready || return 1 + + "$rpm_tool" --version 2>&1 || true + "$rpm_tool" repolist all 2>&1 | sed "s/^/[RPM-REPOLIST] /" || true + + if pkg_bool_true "$PKG_RPM_BEST_EFFORT_CLEAN"; then + "$rpm_tool" clean all 2>&1 | sed "s/^/[RPM-CLEAN] /" || true + fi + + pkg_run_cmd_retry "$rpm_tool-makecache" "$rpm_tool" makecache -y + rpm_update_rc=$? + + if [ "$rpm_update_rc" -eq 0 ]; then + : > "$PKG_RPM_UPDATED_MARK" 2>/dev/null || true + fi + + return "$rpm_update_rc" +} + +# Optionally upgrade rpm packages. +pkg_rpm_upgrade() { + rpm_tool="$(pkg_rpm_tool || true)" + + if [ -z "$rpm_tool" ]; then + return 1 + fi + + if ! pkg_bool_true "$PKG_PACKAGE_UPGRADE"; then + pkg_log_info "$rpm_tool upgrade disabled by config" + return 0 + fi + + if [ -f "$PKG_RPM_UPGRADED_MARK" ]; then + pkg_log_info "$rpm_tool upgrade already completed in this test session" + return 0 + fi + + pkg_rpm_update || return 1 + + pkg_run_cmd_retry "$rpm_tool-upgrade" "$rpm_tool" upgrade -y + rpm_upgrade_rc=$? + + if [ "$rpm_upgrade_rc" -eq 0 ]; then + : > "$PKG_RPM_UPGRADED_MARK" 2>/dev/null || true + fi + + return "$rpm_upgrade_rc" +} + +# Install rpm package. +pkg_rpm_install_package() { + rpm_install_pkg="$1" + rpm_tool="$(pkg_rpm_tool || true)" + + if [ -z "$rpm_tool" ]; then + return 1 + fi + + pkg_rpm_update || return 1 + pkg_rpm_upgrade || return 1 + + "$rpm_tool" info "$rpm_install_pkg" 2>&1 | sed "s/^/[RPM-INFO:$rpm_install_pkg] /" || true + + pkg_run_cmd_retry "$rpm_tool-install-$rpm_install_pkg" "$rpm_tool" install -y "$rpm_install_pkg" +} + +# Refresh opkg metadata. +pkg_opkg_update() { + if [ -f "$PKG_OPKG_UPDATED_MARK" ]; then + pkg_log_info "opkg update already completed in this test session" + return 0 + fi + + pkg_ensure_network_ready || return 1 + + pkg_run_cmd_retry "opkg-update" opkg update + opkg_update_rc=$? + + if [ "$opkg_update_rc" -eq 0 ]; then + : > "$PKG_OPKG_UPDATED_MARK" 2>/dev/null || true + fi + + return "$opkg_update_rc" +} + +# Install opkg package. +pkg_opkg_install_package() { + opkg_install_pkg="$1" + + pkg_opkg_update || return 1 + pkg_run_cmd_retry "opkg-install-$opkg_install_pkg" opkg install "$opkg_install_pkg" +} + +# Install package using active provider. +pkg_install_package() { + install_pkg="$1" + install_provider="$(pkg_active_provider)" + + if [ -z "$install_pkg" ]; then + pkg_log_warn "pkg_install_package called with empty package name" + return 1 + fi + + if pkg_have_package "$install_pkg"; then + pkg_log_package_present "$install_pkg" + pkg_log_package_dependencies "$install_pkg" + return 0 + fi + + if ! pkg_can_install; then + pkg_log_warn "Package missing and package install disabled, $install_pkg" + return 1 + fi + + pkg_provider_summary + + case "$install_provider" in + apt) + pkg_apt_install_package "$install_pkg" + ;; + rpm) + pkg_rpm_install_package "$install_pkg" + ;; + opkg) + pkg_opkg_install_package "$install_pkg" + ;; + check) + pkg_log_warn "Check-only provider does not support package installation" + return 1 + ;; + *) + pkg_log_warn "Unsupported package provider, $install_provider" + return 1 + ;; + esac +} + +# Ensure a single package is installed through the active provider. +pkg_ensure_package() { + ensure_single_pkg="$1" + + if [ -z "$ensure_single_pkg" ]; then + return 1 + fi + + if pkg_have_package "$ensure_single_pkg"; then + pkg_log_package_present "$ensure_single_pkg" + pkg_log_package_dependencies "$ensure_single_pkg" + return 0 + fi + + pkg_install_package "$ensure_single_pkg" +} + +# Upgrade an already-installed package through the active provider. +pkg_upgrade_installed_package() { + upgrade_pkg="$1" + upgrade_provider="$(pkg_active_provider)" + + [ -n "$upgrade_pkg" ] || return 1 + + case "$upgrade_provider" in + apt) + pkg_apt_update || return 1 + pkg_log_info "Checking package-specific upgrade, $upgrade_pkg" + pkg_run_cmd_retry "apt-only-upgrade-$upgrade_pkg" \ + env DEBIAN_FRONTEND=noninteractive \ + "$PKG_APT_GET" install -y \ + --only-upgrade \ + --no-install-recommends \ + -o "DPkg::Lock::Timeout=${PKG_APT_LOCK_TIMEOUT}" \ + "$upgrade_pkg" + ;; + rpm) + pkg_log_info "Checking package-specific rpm upgrade, $upgrade_pkg" + rpm_tool="$(pkg_rpm_tool || true)" + [ -n "$rpm_tool" ] || return 1 + pkg_rpm_update || return 1 + pkg_run_cmd_retry "$rpm_tool-upgrade-$upgrade_pkg" "$rpm_tool" upgrade -y "$upgrade_pkg" + ;; + opkg) + pkg_log_info "Checking package-specific opkg upgrade, $upgrade_pkg" + pkg_opkg_update || return 1 + pkg_run_cmd_retry "opkg-upgrade-$upgrade_pkg" opkg upgrade "$upgrade_pkg" + ;; + *) + pkg_log_info "Provider does not support package upgrade, provider=$upgrade_provider pkg=$upgrade_pkg" + return 0 + ;; + esac +} + +# Install a missing package or upgrade it when package-set upgrade is enabled. +pkg_ensure_or_upgrade_package() { + ensure_pkg="$1" + + [ -n "$ensure_pkg" ] || return 1 + + if pkg_have_package "$ensure_pkg"; then + pkg_log_package_present "$ensure_pkg" + pkg_log_package_dependencies "$ensure_pkg" + + if pkg_bool_true "$PKG_PACKAGE_SET_UPGRADE"; then + if ! pkg_upgrade_installed_package "$ensure_pkg"; then + return 1 + fi + + if pkg_have_package "$ensure_pkg"; then + pkg_log_package_present "$ensure_pkg" + pkg_log_package_dependencies "$ensure_pkg" + fi + fi + + return 0 + fi + + pkg_log_warn "Package missing, installing, $ensure_pkg" + + if ! pkg_install_package "$ensure_pkg"; then + return 1 + fi + + if pkg_have_package "$ensure_pkg"; then + pkg_log_package_present "$ensure_pkg" + pkg_log_package_dependencies "$ensure_pkg" + fi + + return 0 +} + +# Ensure every package in a named package set is installed or upgraded when mapped. +pkg_ensure_package_set() { + set_name="$1" + + if [ -z "$set_name" ]; then + pkg_log_warn "pkg_ensure_package_set called with empty set name" + return 1 + fi + + set_provider="$(pkg_active_provider)" + set_os_id="$(pkg_detect_os_id)" + set_packages="$(pkg_lookup_package_set "$set_name" || true)" + + if [ -z "$set_packages" ]; then + pkg_log_info "Package-set recovery skipped, no mapping for set=$set_name provider=$set_provider os=$set_os_id" + return 0 + fi + + pkg_log_info "Ensuring package set, set=$set_name packages=$set_packages" + + for set_pkg in $set_packages; do + [ -n "$set_pkg" ] || continue + + if ! pkg_ensure_or_upgrade_package "$set_pkg"; then + pkg_log_fail "Failed to ensure package set, set=$set_name pkg=$set_pkg" + return 1 + fi + done + + pkg_log_pass "Package set ready, set=$set_name" + return 0 +} + +# Ensure command is available, recovering missing commands through mapped packages. +pkg_ensure_command() { + ensure_cmd="$1" + shift || true + + if [ -z "$ensure_cmd" ]; then + return 1 + fi + + if pkg_have_command "$ensure_cmd"; then + pkg_log_pass "Command present, $ensure_cmd" + return 0 + fi + + ensure_cmd_packages="$*" + + if [ -z "$ensure_cmd_packages" ]; then + ensure_cmd_packages="$(pkg_lookup_packages_for_command "$ensure_cmd" || true)" + fi + + if [ -z "$ensure_cmd_packages" ]; then + pkg_log_warn "Command missing and no package mapping is available, $ensure_cmd" + return 1 + fi + + pkg_log_warn "Command missing, $ensure_cmd; required package set: $ensure_cmd_packages" + + for required_pkg in $ensure_cmd_packages; do + [ -n "$required_pkg" ] || continue + + if ! pkg_ensure_package "$required_pkg"; then + pkg_log_warn "Failed to install required package for command, cmd=$ensure_cmd pkg=$required_pkg" + return 1 + fi + done + + if pkg_have_command "$ensure_cmd"; then + pkg_log_pass "Command available after package recovery, $ensure_cmd" + return 0 + fi + + pkg_log_warn "Package set installed but command is still missing, cmd=$ensure_cmd packages=$ensure_cmd_packages" + return 1 +} + +# Print provider summary. +pkg_provider_summary() { + summary_provider="$(pkg_active_provider)" + summary_os_id="$(pkg_detect_os_id)" + summary_os_version="$(pkg_os_release_value VERSION_ID || echo unknown)" + + pkg_log_info "Package provider, provider=$summary_provider os=$summary_os_id version=$summary_os_version recover=$PKG_CHECK_DEPS_RECOVER auto_install=$PKG_AUTO_INSTALL upgrade=$PKG_PACKAGE_UPGRADE package_set_upgrade=$PKG_PACKAGE_SET_UPGRADE" + + case "$summary_provider" in + apt) + pkg_log_info "APT provider active, optional_sources_path=$PKG_APT_SOURCES_ARTIFACT_DIR auth_conf=$PKG_APT_AUTH_CONF debusine_source=$PKG_APT_DEBUSINE_SOURCE debusine_suite=$PKG_APT_DEBUSINE_SUITE" + ;; + rpm) + pkg_log_info "RPM provider active" + ;; + opkg) + pkg_log_info "OPKG provider active" + ;; + *) + pkg_log_info "Check-only provider active" + ;; + esac +}