#!/usr/bin/env bash
#
# yum-cross - manage cross-compile sysroot dependencies.
#
# Wraps the manual flow "yumdownloader (target arch RPM) -> rpm2cpio | cpio
# -> fix dangling symlinks -> drop into sysroot" into a single yum-like command:
#
#   yum-cross --arch=aarch64 install zlib-devel openssl-devel libseccomp-devel
#
# Target arches : aarch64 / riscv64 / loongarch64
# Host deps     : yumdownloader (dnf-utils), rpm2cpio, cpio
#
set -euo pipefail

PROG="${0##*/}"

# ---------------------------------------------------------------------------
# Defaults
# ---------------------------------------------------------------------------
ARCH=""
SYSROOT=""
KEEP_RPMS=0        # keep downloaded RPMs after extract
DRY_RUN=0
WORKDIR=""         # RPM download dir (default: mktemp)
SUDO=""            # auto-filled with sudo when sysroot needs root

# C/C++ runtime shipped by the cross toolchain; never pull the distro's target
# arch build of these into the sysroot:
#   - glibc-<triplet>    : puts libc / ld-linux / headers into the sysroot,
#                          the distro package would overwrite and break it;
#   - gcc-libs-<triplet> : libgcc_s / libstdc++ live in /usr/lib/gcc/<triplet>/
#                          and are found by the compiler; a distro copy in the
#                          sysroot risks C++ runtime symbol mismatch.
# binutils is NOT guarded: its cross package only installs host tools under
# /usr/bin and ships nothing into the sysroot, so the distro's target-arch
# binutils/libbfd is a legitimate library to import.
TOOLCHAIN_PROVIDED=(glibc 'glibc-*' libgcc 'libgcc-*' libstdc++ 'libstdc++-*')

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
c_red()  { printf '\033[31m%s\033[0m' "$*"; }
c_grn()  { printf '\033[32m%s\033[0m' "$*"; }
c_ylw()  { printf '\033[33m%s\033[0m' "$*"; }

info()  { echo "$(c_grn "[yum-cross]") $*"; }
warn()  { echo "$(c_ylw "[yum-cross]") $*" >&2; }
die()   { echo "$(c_red "[yum-cross] error:")" "$*" >&2; exit 1; }

# print & run; print only under --dry-run
run() {
    if [ "$DRY_RUN" -eq 1 ]; then
        echo "  + $*"
    else
        eval "$@"
    fi
}

# true if a name (package name or RPM filename) is toolchain-provided
is_toolchain_pkg() {
    local name="$1" pat
    for pat in "${TOOLCHAIN_PROVIDED[@]}"; do
        # shellcheck disable=SC2254  # $pat is an intentional glob
        case "$name" in $pat) return 0 ;; esac
    done
    return 1
}

# drop toolchain-provided entries from the given list (warn on each);
# result goes into the global array GUARDED_ITEMS
GUARDED_ITEMS=()
guard_toolchain_pkgs() {
    GUARDED_ITEMS=()
    local item base
    for item in "$@"; do
        base="${item##*/}"          # match by basename for RPM file paths
        if is_toolchain_pkg "$item" || is_toolchain_pkg "${base%.rpm}"; then
            warn "skip '$item': provided by the cross toolchain (glibc-$TRIPLET, ...), must not go into the sysroot."
        else
            GUARDED_ITEMS+=("$item")
        fi
    done
}

usage() {
    cat <<EOF
$PROG - manage cross-compile sysroot dependencies

Usage:
  $PROG --arch=<arch> [options] <subcommand> [packages...]

Subcommands:
  install <pkgs...>    download target-arch RPMs, extract into sysroot, fix symlinks (default)
  download <pkgs...>   only download RPMs into the workdir, no extract
  extract <rpms...>    extract existing RPM files into sysroot and fix symlinks
  fix-links            scan and fix dangling absolute symlinks in the sysroot
  info                 print current arch / triplet / sysroot config

Options:
  --arch=ARCH          target arch: aarch64 | riscv64 | loongarch64 (required)
  --sysroot=DIR        sysroot path (default: /usr/lib/<triplet>/sys-root)
  --keep-rpms          keep downloaded RPMs after extract (default: remove temp files)
  --workdir=DIR        RPM download/staging dir (default: a mktemp temp dir)
  --dry-run            only print the commands, do not run them
  -h, --help           show this help

Notes:
  Downloads exactly the packages you name; no dependency resolution. For a
  cross sysroot, list the runtime lib together with its -devel (e.g. zlib +
  zlib-devel). glibc / ld-linux etc. are provided by the toolchain package
  glibc-<triplet> and must not be downloaded here.

Examples:
  # list runtime libs + devel packages explicitly
  $PROG --arch=aarch64 install zlib zlib-devel openssl-libs openssl-devel \\
        libseccomp libseccomp-devel

  # custom sysroot
  $PROG --arch=riscv64 --sysroot=/opt/rv64-sysroot install \\
        openssl-libs openssl-devel

  # download only, no extract
  $PROG --arch=aarch64 download glib2-devel

  # fix dangling symlinks in the sysroot
  $PROG --arch=aarch64 fix-links
EOF
}

# arch -> triplet
arch_to_triplet() {
    case "$1" in
        aarch64)     echo "aarch64-linux-gnu" ;;
        riscv64)     echo "riscv64-linux-gnu" ;;
        loongarch64) echo "loongarch64-linux-gnu" ;;
        *) die "unsupported arch '$1' (choose: aarch64 / riscv64 / loongarch64)" ;;
    esac
}

need_cmd() {
    command -v "$1" >/dev/null 2>&1 || die "missing command '$1', install it with: $2"
}

# decide whether writing the sysroot needs sudo
compute_sudo() {
    local target="$1"
    # walk up to the nearest existing parent to test writability
    local probe="$target"
    while [ ! -e "$probe" ] && [ "$probe" != "/" ]; do
        probe="$(dirname "$probe")"
    done
    if [ ! -w "$probe" ]; then
        if command -v sudo >/dev/null 2>&1; then
            SUDO="sudo"
        else
            warn "no write permission on $target and sudo not found; this may fail."
        fi
    fi
}

# rewrite dangling absolute symlinks (e.g. -> /lib64/xxx) to sysroot-relative
fix_broken_symlinks() {
    local root="$1"
    [ -d "$root" ] || { warn "sysroot not found, skip symlink fixup: $root"; return 0; }
    info "fixing dangling absolute symlinks in sysroot: $root"
    if [ "$DRY_RUN" -eq 1 ]; then
        echo "  + (scan & rewrite dangling absolute symlinks under $root)"
        return 0
    fi
    local fixed=0
    # every symlink whose target does not exist
    while IFS= read -r -d '' link; do
        local target
        target="$(readlink "$link")"
        case "$target" in
            /*)
                local rel="${root%/}/${target#/}"
                if [ -e "$rel" ]; then
                    $SUDO ln -sf "$rel" "$link"
                    fixed=$((fixed + 1))
                fi
                ;;
        esac
    done < <($SUDO find "$root" -type l ! -exec test -e {} \; -print0 2>/dev/null)
    info "fixed $fixed symlink(s)."
}

# extract a set of RPMs into the sysroot
extract_rpms() {
    local root="$1"; shift
    local rpms=("$@")
    [ "${#rpms[@]}" -gt 0 ] || die "no RPM to extract."

    guard_toolchain_pkgs "${rpms[@]}"
    rpms=("${GUARDED_ITEMS[@]}")
    [ "${#rpms[@]}" -gt 0 ] || die "no RPM to extract: all given files are toolchain-provided packages."

    run "$SUDO mkdir -p '$root'"
    local rpm
    for rpm in "${rpms[@]}"; do
        [ -f "$rpm" ] || { warn "skip missing file: $rpm"; continue; }
        info "extract $(basename "$rpm") -> $root"
        if [ "$DRY_RUN" -eq 1 ]; then
            echo "  + rpm2cpio '$rpm' | $SUDO cpio -idm -D '$root'"
        else
            rpm2cpio "$rpm" | $SUDO cpio -idm -D "$root" --quiet
        fi
    done
    fix_broken_symlinks "$root"
}

# download target-arch RPMs into destdir; result -> global DOWNLOADED_RPMS
DOWNLOADED_RPMS=()
download_rpms() {
    local destdir="$1"; shift
    local pkgs=("$@")
    [ "${#pkgs[@]}" -gt 0 ] || die "no package specified to download."

    guard_toolchain_pkgs "${pkgs[@]}"
    pkgs=("${GUARDED_ITEMS[@]}")
    [ "${#pkgs[@]}" -gt 0 ] || die "nothing to download: all specified packages are toolchain-provided."

    run "mkdir -p '$destdir'"

    info "downloading $ARCH RPMs: ${pkgs[*]}"
    run "yumdownloader --forcearch='$ARCH' --destdir='$destdir' ${pkgs[*]}"

    if [ "$DRY_RUN" -eq 1 ]; then
        DOWNLOADED_RPMS=()
        return 0
    fi

    mapfile -t DOWNLOADED_RPMS < <(find "$destdir" -maxdepth 1 -name '*.rpm' -type f 2>/dev/null)
    [ "${#DOWNLOADED_RPMS[@]}" -gt 0 ] || die "no RPM downloaded; check package names or repo config."
    info "downloaded ${#DOWNLOADED_RPMS[@]} RPM(s)."
}

cleanup_workdir=""
cleanup() {
    [ -n "$cleanup_workdir" ] && [ "$KEEP_RPMS" -eq 0 ] && rm -rf "$cleanup_workdir" 2>/dev/null || true
}
trap cleanup EXIT

# ---------------------------------------------------------------------------
# Arg parsing
# ---------------------------------------------------------------------------
ARGS=()
while [ $# -gt 0 ]; do
    case "$1" in
        --arch=*)     ARCH="${1#*=}" ;;
        --arch)       shift; ARCH="${1:-}" ;;
        --sysroot=*)  SYSROOT="${1#*=}" ;;
        --sysroot)    shift; SYSROOT="${1:-}" ;;
        --workdir=*)  WORKDIR="${1#*=}" ;;
        --workdir)    shift; WORKDIR="${1:-}" ;;
        --keep-rpms)  KEEP_RPMS=1 ;;
        --dry-run)    DRY_RUN=1 ;;
        -h|--help)    usage; exit 0 ;;
        --)           shift; while [ $# -gt 0 ]; do ARGS+=("$1"); shift; done; break ;;
        -*)           die "unknown option: $1 (see -h for help)" ;;
        *)            ARGS+=("$1") ;;
    esac
    shift
done

[ "${#ARGS[@]}" -gt 0 ] || { usage; exit 1; }

SUBCMD="${ARGS[0]}"
PKGS=("${ARGS[@]:1}")

# ---------------------------------------------------------------------------
# Validate + derive config
# ---------------------------------------------------------------------------
[ -n "$ARCH" ] || die "target arch is required via --arch (aarch64 / riscv64 / loongarch64)"
TRIPLET="$(arch_to_triplet "$ARCH")"
[ -n "$SYSROOT" ] || SYSROOT="/usr/lib/${TRIPLET}/sys-root"
compute_sudo "$SYSROOT"

# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
case "$SUBCMD" in
    info)
        echo "  arch     : $ARCH"
        echo "  triplet  : $TRIPLET"
        echo "  sysroot  : $SYSROOT"
        echo "  sudo     : $([ -n "$SUDO" ] && echo "yes (sysroot needs root)" || echo "no")"
        ;;

    install)
        [ "${#PKGS[@]}" -gt 0 ] || die "install needs at least one package name."
        need_cmd yumdownloader "yum install dnf-utils"
        need_cmd rpm2cpio "yum install rpm"
        need_cmd cpio "yum install cpio"

        if [ -n "$WORKDIR" ]; then
            destdir="$WORKDIR"
        else
            destdir="$(mktemp -d /tmp/yum-cross.XXXXXX)"
            cleanup_workdir="$destdir"
        fi

        download_rpms "$destdir" "${PKGS[@]}"
        if [ "$DRY_RUN" -eq 1 ]; then
            info "(dry-run) would extract $destdir/*.rpm into $SYSROOT and fix symlinks."
        else
            extract_rpms "$SYSROOT" "${DOWNLOADED_RPMS[@]}"
        fi
        info "done. sysroot ready: $SYSROOT"
        info "compile with: --sysroot=$SYSROOT ; run under QEMU with: -L $SYSROOT"
        ;;

    download)
        [ "${#PKGS[@]}" -gt 0 ] || die "download needs at least one package name."
        need_cmd yumdownloader "yum install dnf-utils"
        destdir="${WORKDIR:-$PWD}"
        KEEP_RPMS=1   # pure download always keeps the RPMs
        download_rpms "$destdir" "${PKGS[@]}"
        info "RPMs saved to: $destdir"
        ;;

    extract)
        [ "${#PKGS[@]}" -gt 0 ] || die "extract needs at least one RPM file path."
        need_cmd rpm2cpio "yum install rpm"
        need_cmd cpio "yum install cpio"
        extract_rpms "$SYSROOT" "${PKGS[@]}"
        info "extract done: $SYSROOT"
        ;;

    fix-links)
        fix_broken_symlinks "$SYSROOT"
        ;;

    *)
        die "unknown subcommand '$SUBCMD' (available: install / download / extract / fix-links / info)"
        ;;
esac
