#!/bin/bash
#
# setup-linux.sh -- install everything needed to build rsync on Ubuntu.
#
# Tested on Ubuntu 22.04.5 LTS and 26.04 LTS.  Covers both build systems:
# the autoconf one (./configure && make) and the CMake one (CMakeLists.txt),
# plus the optional feature libraries and the manpage generator.
#
# Usage:
#     ./setup-linux.sh              # install required + optional
#     ./setup-linux.sh --minimal    # required only (no ACLs/xattrs/zstd/...)
#     ./setup-linux.sh --dry-run    # show what would be installed
#
# Safe to re-run: apt skips anything already present.

set -uo pipefail

MINIMAL=0
DRY_RUN=0

for arg in "$@"; do
    case "$arg" in
    --minimal)  MINIMAL=1 ;;
    --dry-run)  DRY_RUN=1 ;;
    -h|--help)
        sed -n '2,17p' "$0" | sed 's/^# \{0,1\}//'
        exit 0 ;;
    *)
        echo "setup-linux.sh: unknown option '$arg' (try --help)" >&2
        exit 2 ;;
    esac
done

# ---------------------------------------------------------------- helpers

red()   { printf '\033[31m%s\033[0m\n' "$*"; }
green() { printf '\033[32m%s\033[0m\n' "$*"; }
bold()  { printf '\033[1m%s\033[0m\n' "$*"; }

die() { red "ERROR: $*"; exit 1; }

# ------------------------------------------------------- sanity checks

command -v apt-get >/dev/null 2>&1 \
    || die "no apt-get found -- this script is for Debian/Ubuntu systems."

DISTRO="unknown"
RELEASE="unknown"
if [ -r /etc/os-release ]; then
    # shellcheck disable=SC1091  # runtime file, not available to the linter
    . /etc/os-release
    DISTRO="${ID:-unknown}"
    RELEASE="${VERSION_ID:-unknown}"
fi

bold "rsync build dependencies"
echo "  distro ......... ${PRETTY_NAME:-$DISTRO $RELEASE}"
echo "  architecture ... $(uname -m)"

case "$DISTRO" in
ubuntu|debian|linuxmint|pop) ;;
*)  echo
    red "Warning: this script targets Ubuntu/Debian; '$DISTRO' is untested."
    echo "Continuing anyway -- package names may differ."
    ;;
esac

# apt needs root.  Re-exec under sudo rather than sprinkling it around, so
# that a single password prompt covers the whole run.
if [ "$(id -u)" -ne 0 ] && [ "$DRY_RUN" -eq 0 ]; then
    command -v sudo >/dev/null 2>&1 \
        || die "not running as root and sudo is not installed."
    echo
    echo "Re-running under sudo..."
    # Plain sudo, not "sudo -E": some sudoers configs refuse to preserve the
    # environment, and the exports below happen in the root instance anyway.
    exec sudo "$0" "$@"
fi

export DEBIAN_FRONTEND=noninteractive
export NEEDRESTART_MODE=a          # don't prompt about restarting services

# ------------------------------------------------------------- packages

# Needed to build rsync at all.
REQUIRED=(
    build-essential     # gcc, g++, make, libc headers
    gawk                # rsync needs a modern awk for its code generators
    autoconf            # ./configure is generated from configure.ac
    automake
    python3             # manpage + header generators, and the test suite
    cmake               # the CMake build
    ninja-build         # ...and its default generator
    git                 # version stamping (git-version.h)
    pkg-config
)

# Optional: each one switches on an rsync feature.  Missing ones only mean
# a smaller feature set, never a failed build.
OPTIONAL=(
    acl libacl1-dev         # --acls  (helper tools also used by the tests)
    attr libattr1-dev       # --xattrs (likewise)
    libxxhash-dev           # xxhash checksums (becomes the default)
    libzstd-dev             # zstd compression (becomes the default)
    liblz4-dev              # lz4 compression
    libssl-dev              # OpenSSL MD4/MD5
    zlib1g-dev              # for cmake -DRSYNC_EXTERNAL_ZLIB=ON
    libpopt-dev             # for ./configure --with-included-popt=no
)

# Development tooling: not needed to build rsync, but useful when working on
# its shell scripts -- including this one, which shellcheck keeps honest.
DEVTOOLS=(
    shellcheck
)
OPTIONAL+=("${DEVTOOLS[@]}")

# The manpages need one of two python3 markdown libraries; upstream prefers
# cmarkgfm.  Pick whichever this release actually offers.
MARKDOWN_PKG=""
for p in python3-cmarkgfm python3-commonmark; do
    cand=$(apt-cache policy "$p" 2>/dev/null | awk '/Candidate:/{print $2}')
    if [ -n "$cand" ] && [ "$cand" != "(none)" ]; then
        MARKDOWN_PKG="$p"
        break
    fi
done
if [ -n "$MARKDOWN_PKG" ]; then
    OPTIONAL+=("$MARKDOWN_PKG")
fi

if [ "$MINIMAL" -eq 1 ]; then
    OPTIONAL=()
fi

if [ "$DRY_RUN" -eq 1 ]; then
    echo
    bold "Would install (required):"
    printf '  %s\n' "${REQUIRED[@]}"
    if [ ${#OPTIONAL[@]} -gt 0 ]; then
        bold "Would install (optional):"
        printf '  %s\n' "${OPTIONAL[@]}"
    fi
    exit 0
fi

# ------------------------------------------------------------- install

echo
bold "Updating package lists..."
if ! apt-get update -qq; then
    red "apt-get update failed -- continuing with the cached lists."
fi

echo
bold "Installing required packages..."
if ! apt-get install -y "${REQUIRED[@]}"; then
    # Fall back to one-at-a-time so the failing package is obvious.
    red "Bulk install failed; retrying individually to find the culprit..."
    FAILED=()
    for p in "${REQUIRED[@]}"; do
        apt-get install -y "$p" || FAILED+=("$p")
    done
    [ ${#FAILED[@]} -eq 0 ] || die "could not install: ${FAILED[*]}"
fi

OPT_FAILED=()
if [ ${#OPTIONAL[@]} -gt 0 ]; then
    echo
    bold "Installing optional packages..."
    if ! apt-get install -y "${OPTIONAL[@]}"; then
        red "Bulk install failed; retrying individually..."
        for p in "${OPTIONAL[@]}"; do
            apt-get install -y "$p" || OPT_FAILED+=("$p")
        done
    fi
fi

# --------------------------------------------------------------- verify

echo
bold "Verifying the toolchain..."

MISSING=()
check_cmd() {
    if command -v "$1" >/dev/null 2>&1; then
        printf '  %-16s %s\n' "$1" "$(command -v "$1")"
    else
        printf '  %-16s %s\n' "$1" "MISSING"
        MISSING+=("$1")
    fi
}

for c in gcc g++ make gawk autoconf automake cmake ninja python3 git; do
    check_cmd "$c"
done

# Not required to build, so report it without failing the run.
if command -v shellcheck >/dev/null 2>&1; then
    printf '  %-16s %s\n' "shellcheck" "$(command -v shellcheck)"
else
    printf '  %-16s %s\n' "shellcheck" "not installed (optional)"
fi

echo
bold "Verifying optional libraries..."
check_header() {
    # $1 = human name, $2 = header path
    if [ -e "/usr/include/$2" ] || \
       find /usr/include -maxdepth 3 -name "$(basename "$2")" -print -quit \
            2>/dev/null | grep -q .; then
        printf '  %-16s yes\n' "$1"
    else
        printf '  %-16s no\n' "$1"
    fi
}
check_header acl        sys/acl.h
check_header xattr      sys/xattr.h
check_header xxhash     xxhash.h
check_header zstd       zstd.h
check_header lz4        lz4.h
check_header openssl    openssl/md5.h
check_header zlib       zlib.h

echo
bold "Verifying the manpage generator..."
MD_OK=0
for m in cmarkgfm commonmark; do
    if python3 -c "import $m" >/dev/null 2>&1; then
        echo "  python3 module '$m' importable"
        MD_OK=1
        break
    fi
done
if [ "$MD_OK" -eq 0 ]; then
    if [ "$MINIMAL" -eq 1 ]; then
        echo "  skipped (--minimal); build with ./configure --disable-md2man"
    else
        red "  neither cmarkgfm nor commonmark is importable."
        echo "  Install one with:  python3 -mpip install --user commonmark"
        echo "  ...or build with:  ./configure --disable-md2man"
    fi
fi

# ---------------------------------------------------------------- done

echo
if [ ${#MISSING[@]} -ne 0 ]; then
    red "Missing required tools: ${MISSING[*]}"
    exit 1
fi

if [ ${#OPT_FAILED[@]} -ne 0 ]; then
    red "Optional packages that failed to install: ${OPT_FAILED[*]}"
    echo "The build will still work, with those features disabled."
fi

green "All required build dependencies are installed."
cat <<'EOF'

Build rsync with either build system:

  autoconf:   ./configure && make
  CMake:      cmake -B build -G Ninja && cmake --build build

Run the test suite (needs the autoconf build's helper programs):

  make check
EOF
